From 6ebf95c3cc0385e950906c0d5fe6a8fdd22fab41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98topepo=E2=80=99?= Date: Sun, 24 Nov 2024 16:54:57 -0500 Subject: [PATCH 01/13] enable algorithms via pseudocode extension --- MathJax.js | 18 ++ _extensions/leovan/pseudocode/_extension.yml | 7 + _extensions/leovan/pseudocode/pseudocode.lua | 289 ++++++++++++++++++ .../leovan/pseudocode/pseudocode.min.css | 1 + .../leovan/pseudocode/pseudocode.min.js | 1 + .../index/execute-results/html.json | 15 + _quarto.yml | 13 +- contribute/index.html.md | 2 + help/index.html.md | 2 + learn/index-listing.json | 1 + learn/index.html.md | 3 + learn/models/sub-sampling/index.html.md | 4 +- learn/statistics/xtabs/index.html.md | 8 +- learn/work/nested-resampling/index.html.md | 4 +- .../pseudocode-2.4.1/pseudocode.min.css | 1 + .../pseudocode-2.4.1/pseudocode.min.js | 1 + .../quarto-syntax-highlighting.css | 2 + site_libs/quarto-html/quarto.js | 21 +- site_libs/quarto-nav/quarto-nav.js | 1 + site_libs/quarto-search/quarto-search.js | 76 ++++- 20 files changed, 442 insertions(+), 28 deletions(-) create mode 100644 MathJax.js create mode 100644 _extensions/leovan/pseudocode/_extension.yml create mode 100644 _extensions/leovan/pseudocode/pseudocode.lua create mode 100644 _extensions/leovan/pseudocode/pseudocode.min.css create mode 100644 _extensions/leovan/pseudocode/pseudocode.min.js create mode 100644 _freeze/learn/work/efficient-grid-search/index/execute-results/html.json create mode 100644 site_libs/quarto-contrib/pseudocode-2.4.1/pseudocode.min.css create mode 100644 site_libs/quarto-contrib/pseudocode-2.4.1/pseudocode.min.js diff --git a/MathJax.js b/MathJax.js new file mode 100644 index 00000000..c1fc36f9 --- /dev/null +++ b/MathJax.js @@ -0,0 +1,18 @@ + + diff --git a/_extensions/leovan/pseudocode/_extension.yml b/_extensions/leovan/pseudocode/_extension.yml new file mode 100644 index 00000000..df7b2c27 --- /dev/null +++ b/_extensions/leovan/pseudocode/_extension.yml @@ -0,0 +1,7 @@ +title: Pseudocode +author: 范叶亮 | Leo Van +version: 1.1.1 +quarto-required: ">=1.4.0" +contributes: + filters: + - pseudocode.lua diff --git a/_extensions/leovan/pseudocode/pseudocode.lua b/_extensions/leovan/pseudocode/pseudocode.lua new file mode 100644 index 00000000..73ad1bee --- /dev/null +++ b/_extensions/leovan/pseudocode/pseudocode.lua @@ -0,0 +1,289 @@ +local function ensure_html_deps() + quarto.doc.add_html_dependency({ + name = "pseudocode", + version = "2.4.1", + scripts = { "pseudocode.min.js" }, + stylesheets = { "pseudocode.min.css" } + }) + quarto.doc.include_text("after-body", [[ + + ]]) +end + +local function ensure_latex_deps() + quarto.doc.use_latex_package("algorithm") + quarto.doc.use_latex_package("algpseudocode") + quarto.doc.use_latex_package("caption") +end + +local function extract_source_code_options(source_code, render_type) + local options = {} + local source_codes = {} + local found_source_code = false + + for str in string.gmatch(source_code, "([^\n]*)\n?") do + if (string.match(str, "^%s*#|.*") or string.gsub(str, "%s", "") == "") and not found_source_code then + if string.match(str, "^%s*#|%s+[" .. render_type .. "|label].*") then + str = string.gsub(str, "^%s*#|%s+", "") + local idx_start, idx_end = string.find(str, ":%s*") + + if idx_start and idx_end and idx_end + 1 < #str then + k = string.sub(str, 1, idx_start - 1) + v = string.sub(str, idx_end + 1) + v = string.gsub(v, "^\"%s*", "") + v = string.gsub(v, "%s*\"$", "") + + options[k] = v + else + quarto.log.warning("Invalid pseducode option: " .. str) + end + end + else + found_source_code = true + table.insert(source_codes, str) + end + end + + return options, table.concat(source_codes, "\n") +end + +local function render_pseudocode_block_html(global_options) + ensure_html_deps() + + local filter = { + CodeBlock = function(el) + if not el.attr.classes:includes("pseudocode") then + return el + end + + local options, source_code = extract_source_code_options(el.text, "html") + + source_code = string.gsub(source_code, "%s*\\begin{algorithm}[^\n]+", "\\begin{algorithm}") + source_code = string.gsub(source_code, "%s*\\begin{algorithmic}[^\n]+", "\\begin{algorithmic}") + + local alg_id = options["label"] + options["label"] = nil + options["html-caption-prefix"] = global_options.caption_prefix + + if global_options.number_with_in_chapter and global_options.html_chapter_level then + options["html-chapter-level"] = global_options.html_chapter_level + end + + if global_options.caption_number then + options["html-pseudocode-number"] = global_options.html_current_number + end + + local data_options = {} + for k, v in pairs(options) do + if string.match(k, "^html-") then + data_k = string.gsub(k, "^html", "data") + data_options[data_k] = v + end + end + + local inner_el = pandoc.Div(source_code) + inner_el.attr.classes = pandoc.List() + inner_el.attr.classes:insert("pseudocode") + + local outer_el = pandoc.Div(inner_el) + outer_el.attr.classes = pandoc.List() + outer_el.attr.classes:insert("pseudocode-container") + outer_el.attr.classes:insert("quarto-float") + outer_el.attr.attributes = data_options + + if alg_id then + outer_el.attr.identifier = alg_id + global_options.html_identifier_number_mapping[alg_id] = global_options.html_current_number + global_options.html_current_number = global_options.html_current_number + 1 + end + + return outer_el + end + } + + return filter +end + +local function render_pseudocode_block_latex(global_options) + ensure_latex_deps() + + if global_options.caption_number then + quarto.doc.include_text("before-body", "\\floatname{algorithm}{" .. global_options.caption_prefix .. "}") + else + quarto.doc.include_text("in-header", "\\DeclareCaptionLabelFormat{algnonumber}{" .. global_options.caption_prefix .. "}") + quarto.doc.include_text("before-body", "\\captionsetup[algorithm]{labelformat=algnonumber}") + end + + if global_options.number_with_in_chapter then + quarto.doc.include_text("before-body", "\\numberwithin{algorithm}{chapter}") + end + + local filter = { + CodeBlock = function(el) + if not el.attr.classes:includes("pseudocode") then + return el + end + + local options, source_code = extract_source_code_options(el.text, "pdf") + + local pdf_placement = "H" + if options["pdf-placement"] then + pdf_placement = options["pdf-placement"] + end + source_code = string.gsub(source_code, "\\begin{algorithm}%s*\n", "\\begin{algorithm}[" .. pdf_placement .. "]\n") + + if not options["pdf-line-number"] or options["pdf-line-number"] == "true" then + source_code = string.gsub(source_code, "\\begin{algorithmic}%s*\n", "\\begin{algorithmic}[1]\n") + end + + if options["label"] then + source_code = string.gsub(source_code, "\\begin{algorithmic}", "\\label{" .. options["label"] .. "}\n\\begin{algorithmic}") + end + + return pandoc.RawInline("latex", source_code) + end + } + + return filter +end + +local function render_pseudocode_block(global_options) + local filter = { + CodeBlock = function(el) + return el + end + } + + if quarto.doc.is_format("html") then + filter = render_pseudocode_block_html(global_options) + elseif quarto.doc.is_format("latex") then + filter = render_pseudocode_block_latex(global_options) + end + + return filter +end + +local function render_pseudocode_ref_html(global_options) + local filter = { + Cite = function(el) + local cite_text = pandoc.utils.stringify(el.content) + + for k, v in pairs(global_options.html_identifier_number_mapping) do + if cite_text == "@" .. k then + local link_src = "#" .. k + local alg_id = v + + if global_options.html_chapter_level then + alg_id = global_options.html_chapter_level .. "." .. alg_id + end + + local link_text = global_options.reference_prefix .. " " .. alg_id + local link = pandoc.Link(link_text, link_src) + link.attr.classes = pandoc.List() + link.attr.classes:insert("quarto-xref") + + return link + end + end + end + } + + return filter +end + +local function render_pseudocode_ref_latex(global_options) + local filter = { + Cite = function(el) + local cite_text = pandoc.utils.stringify(el.content) + + if string.match(cite_text, "^@alg-") then + return pandoc.RawInline("latex", global_options.reference_prefix .. "~\\ref{" .. string.gsub(cite_text, "^@", "") .. "}" ) + end + end + } + + return filter +end + +local function render_pseudocode_ref(global_options) + local filter = { + Cite = function(el) + return el + end + } + + if quarto.doc.is_format("html") then + filter = render_pseudocode_ref_html(global_options) + elseif quarto.doc.is_format("latex") then + filter = render_pseudocode_ref_latex(global_options) + end + + return filter +end + +function Pandoc(doc) + local global_options = { + caption_prefix = "Algorithm", + reference_prefix = "Algorithm", + caption_number = true, + number_with_in_chapter = false, + html_chapter_level = nil, + html_current_number = 1, + html_identifier_number_mapping = {} + } + + if doc.meta["pseudocode"] then + global_options.caption_prefix = pandoc.utils.stringify(doc.meta["pseudocode"]["caption-prefix"]) or global_options.caption_prefix + global_options.reference_prefix = pandoc.utils.stringify(doc.meta["pseudocode"]["reference-prefix"]) or global_options.reference_prefix + global_options.caption_number = doc.meta["pseudocode"]["caption-number"] or global_options.caption_number + end + + if doc.meta["book"] then + global_options.number_with_in_chapter = true + + if quarto.doc.is_format("html") then + local _, input_qmd_filename = string.match(quarto.doc["input_file"], "^(.-)([^\\/]-%.([^\\/%.]-))$") + local renders = doc.meta["book"]["render"] + + for _, render in pairs(renders) do + if render["file"] and render["number"] and pandoc.utils.stringify(render["file"]) == input_qmd_filename then + global_options.html_chapter_level = pandoc.utils.stringify(render["number"]) + end + end + end + end + + doc = doc:walk(render_pseudocode_block(global_options)) + + return doc:walk(render_pseudocode_ref(global_options)) +end diff --git a/_extensions/leovan/pseudocode/pseudocode.min.css b/_extensions/leovan/pseudocode/pseudocode.min.css new file mode 100644 index 00000000..c28ebfe4 --- /dev/null +++ b/_extensions/leovan/pseudocode/pseudocode.min.css @@ -0,0 +1 @@ +@import url(https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.css);.ps-root{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-size:1em;font-weight:100;-webkit-font-smoothing:antialiased!important}.ps-root .ps-algorithm{margin:.8em 0;border-top:3px solid #000;border-bottom:2px solid #000}.ps-root .ps-algorithm.with-caption>.ps-line:first-child{border-bottom:2px solid #000}.ps-root .katex{text-indent:0;font-size:1em}.ps-root .MathJax,.ps-root .MathJax_CHTML{text-indent:0;font-size:1em!important}.ps-root .ps-line{margin:0;padding:0;line-height:1.2}.ps-root .ps-funcname{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-weight:400;font-variant:small-caps;font-style:normal;text-transform:none}.ps-root .ps-keyword{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-weight:700;font-variant:normal;font-style:normal;text-transform:none}.ps-root .ps-comment{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-weight:400;font-variant:normal;font-style:normal;text-transform:none}.ps-root .ps-linenum{font-size:.8em;line-height:1em;width:1.6em;text-align:right;display:inline-block;position:relative;padding-right:.3em}.ps-root .ps-algorithmic.with-linenum .ps-line.ps-code{text-indent:-1.6em}.ps-root .ps-algorithmic.with-linenum .ps-line.ps-code>span{text-indent:0}.ps-root .ps-algorithmic.with-scopelines div.ps-block{border-left-style:solid;border-left-width:.1em;padding-left:.6em}.ps-root .ps-algorithmic.with-scopelines>div.ps-block{border-left:none} \ No newline at end of file diff --git a/_extensions/leovan/pseudocode/pseudocode.min.js b/_extensions/leovan/pseudocode/pseudocode.min.js new file mode 100644 index 00000000..e19fe9e0 --- /dev/null +++ b/_extensions/leovan/pseudocode/pseudocode.min.js @@ -0,0 +1 @@ +(function(e){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=e()}else if(typeof define==="function"&&define.amd){define([],e)}else{var t;if(typeof window!=="undefined"){t=window}else if(typeof global!=="undefined"){t=global}else if(typeof self!=="undefined"){t=self}else{t=this}t.pseudocode=e()}})(function(){var e,t,n;return function(){function p(o,s,a){function l(n,e){if(!s[n]){if(!o[n]){var t="function"==typeof require&&require;if(!e&&t)return t(n,!0);if(h)return h(n,!0);var i=new Error("Cannot find module '"+n+"'");throw i.code="MODULE_NOT_FOUND",i}var r=s[n]={exports:{}};o[n][0].call(r.exports,function(e){var t=o[n][1][e];return l(t||e)},r,r.exports,p,o,s,a)}return s[n].exports}for(var h="function"==typeof require&&require,e=0;ethis.renderElement(e,t))}}},{"./src/Lexer":2,"./src/ParseError":3,"./src/Parser":4,"./src/Renderer":5}],2:[function(e,t,n){var i=e("./utils");var u=e("./ParseError");var r=function(e){this._input=e;this._remain=e;this._pos=0;this._nextAtom=this._currentAtom=null;this._next()};r.prototype.accept=function(e,t){if(this._nextAtom.type===e&&this._matchText(t)){this._next();return this._currentAtom.text}return null};r.prototype.expect=function(e,t){var n=this._nextAtom;if(n.type!==e){throw new u(`Expected an atom of ${e} but received ${n.type}`,this._pos,this._input)}if(!this._matchText(t)){throw new u(`Expected \`${t}\` but received \`${n.text}\``,this._pos,this._input)}this._next();return this._currentAtom.text};r.prototype.get=function(){return this._currentAtom};var o={exec:function(e){var t=[{start:"$",end:"$"},{start:"\\(",end:"\\)"}];var n=e.length;for(var i=0;i0&&a[l-1]==="\\"){var h=l+o.length;a=a.slice(h);s+=h;continue}var p=[e.slice(0,s+l+o.length),e.slice(r.length,s+l)];return p}}return null}};var p={special:/^(\\\\|\\{|\\}|\\\$|\\&|\\#|\\%|\\_)/,math:o,func:/^\\([a-zA-Z]+)/,open:/^\{/,close:/^\}/,quote:/^(`|``|'|'')/,ordinary:/^[^\\{}$&#%_\s]+/};var c=/^%.*/;var f=/^\s+/;r.prototype._skip=function(e){this._pos+=e;this._remain=this._remain.slice(e)};r.prototype._next=function(){var e=false;while(1){var t=f.exec(this._remain);if(t){e=true;var n=t[0].length;this._skip(n)}var i=c.exec(this._remain);if(!i)break;var r=i[0].length;this._skip(r)}this._currentAtom=this._nextAtom;if(this._remain===""){this._nextAtom={type:"EOF",text:null,whitespace:false};return false}for(var o in p){var s=p[o];var a=s.exec(this._remain);if(!a)continue;var l=a[0];var h=a[1]?a[1]:l;this._nextAtom={type:o,text:h,whitespace:e};this._pos+=l.length;this._remain=this._remain.slice(a[0].length);return true}throw new u("Unrecoganizable atom",this._pos,this._input)};r.prototype._matchText=function(e){if(e===null||e===undefined)return true;if(i.isString(e))return e.toLowerCase()===this._nextAtom.text.toLowerCase();else return e.some(e=>e.toLowerCase()===this._nextAtom.text.toLowerCase())};t.exports=r},{"./ParseError":3,"./utils":6}],3:[function(e,t,n){function i(e,t,n){var i=`Error: ${e}`;if(t!==undefined&&n!==undefined){i+=` at position ${t}: \``;n=`${n.slice(0,t)}\u21B1${n.slice(t)}`;var r=Math.max(0,t-15);var o=t+15;i+=`${n.slice(r,o)}\``}this.message=i}i.prototype=Object.create(Error.prototype);i.prototype.constructor=i;t.exports=i},{}],4:[function(e,t,n){var s=e("./utils");var r=e("./ParseError");var a=function(e,t){this.type=e;this.value=t;this.children=[]};a.prototype.toString=function(e){if(!e)e=0;var t="";for(var n=0;n`;if(this.value)i+=` (${s.toString(this.value)})`;i+="\n";if(this.children){for(var r=0;r0){e.addChild(t);continue}break}return e};i.prototype._parseCaption=function(){var e=this._lexer;if(!e.accept("func","caption"))return null;var t=new a("caption");e.expect("open");t.addChild(this._parseCloseText());e.expect("close");return t};i.prototype._parseBlock=function(){var e=new a("block");while(true){var t=this._parseControl();if(t){e.addChild(t);continue}var n=this._parseFunction();if(n){e.addChild(n);continue}var i=this._parseStatement(h);if(i){e.addChild(i);continue}var r=this._parseCommand(p);if(r){e.addChild(r);continue}var o=this._parseComment();if(o){e.addChild(o);continue}break}return e};i.prototype._parseControl=function(){var e;if(e=this._parseIf())return e;if(e=this._parseLoop())return e;if(e=this._parseRepeat())return e;if(e=this._parseUpon())return e};i.prototype._parseFunction=function(){var e=this._lexer;if(!e.accept("func",["function","procedure"]))return null;var t=this._lexer.get().text;e.expect("open");var n=e.expect("ordinary");e.expect("close");e.expect("open");var i=this._parseCloseText();e.expect("close");var r=this._parseBlock();e.expect("func",`end${t}`);var o=new a("function",{type:t,name:n});o.addChild(i);o.addChild(r);return o};i.prototype._parseIf=function(){if(!this._lexer.accept("func","if"))return null;var e=new a("if");this._lexer.expect("open");e.addChild(this._parseCond());this._lexer.expect("close");e.addChild(this._parseBlock());var t=0;while(this._lexer.accept("func",["elif","elsif","elseif"])){this._lexer.expect("open");e.addChild(this._parseCond());this._lexer.expect("close");e.addChild(this._parseBlock());t++}var n=false;if(this._lexer.accept("func","else")){n=true;e.addChild(this._parseBlock())}this._lexer.expect("func","endif");e.value={numElif:t,hasElse:n};return e};i.prototype._parseLoop=function(){if(!this._lexer.accept("func",["FOR","FORALL","WHILE"]))return null;var e=this._lexer.get().text.toLowerCase();var t=new a("loop",e);this._lexer.expect("open");t.addChild(this._parseCond());this._lexer.expect("close");t.addChild(this._parseBlock());var n=e!=="forall"?`end${e}`:"endfor";this._lexer.expect("func",n);return t};i.prototype._parseRepeat=function(){if(!this._lexer.accept("func",["REPEAT"]))return null;var e=this._lexer.get().text.toLowerCase();var t=new a("repeat",e);t.addChild(this._parseBlock());this._lexer.expect("func","until");this._lexer.expect("open");t.addChild(this._parseCond());this._lexer.expect("close");return t};i.prototype._parseUpon=function(){if(!this._lexer.accept("func","upon"))return null;var e=new a("upon");this._lexer.expect("open");e.addChild(this._parseCond());this._lexer.expect("close");e.addChild(this._parseBlock());this._lexer.expect("func","endupon");return e};var l=["ensure","require","input","output"];var h=["state","print","return"];i.prototype._parseStatement=function(e){if(!this._lexer.accept("func",e))return null;var t=this._lexer.get().text.toLowerCase();var n=new a("statement",t);n.addChild(this._parseOpenText());return n};var p=["break","continue"];i.prototype._parseCommand=function(e){if(!this._lexer.accept("func",e))return null;var t=this._lexer.get().text.toLowerCase();var n=new a("command",t);return n};i.prototype._parseComment=function(){if(!this._lexer.accept("func","comment"))return null;var e=new a("comment");this._lexer.expect("open");e.addChild(this._parseCloseText());this._lexer.expect("close");return e};i.prototype._parseCall=function(){var e=this._lexer;if(!e.accept("func","call"))return null;var t=e.get().whitespace;e.expect("open");var n=e.expect("ordinary");e.expect("close");var i=new a("call");i.whitespace=t;i.value=n;e.expect("open");var r=this._parseCloseText();i.addChild(r);e.expect("close");return i};i.prototype._parseCond=i.prototype._parseCloseText=function(){return this._parseText("close")};i.prototype._parseOpenText=function(){return this._parseText("open")};i.prototype._parseText=function(e){var t=new a(`${e}-text`);var n=false;var i;while(true){i=this._parseAtom()||this._parseCall();if(i){if(n)i.whitespace|=n;t.addChild(i);continue}if(this._lexer.accept("open")){i=this._parseCloseText();n=this._lexer.get().whitespace;i.whitespace=n;t.addChild(i);this._lexer.expect("close");n=this._lexer.get().whitespace;continue}break}return t};var u={ordinary:{tokenType:"ordinary"},math:{tokenType:"math"},special:{tokenType:"special"},"cond-symbol":{tokenType:"func",tokenValues:["and","or","not","true","false","to","downto"]},"quote-symbol":{tokenType:"quote"},"sizing-dclr":{tokenType:"func",tokenValues:["tiny","scriptsize","footnotesize","small","normalsize","large","Large","LARGE","huge","Huge"]},"font-dclr":{tokenType:"func",tokenValues:["normalfont","rmfamily","sffamily","ttfamily","upshape","itshape","slshape","scshape","bfseries","mdseries","lfseries"]},"font-cmd":{tokenType:"func",tokenValues:["textnormal","textrm","textsf","texttt","textup","textit","textsl","textsc","uppercase","lowercase","textbf","textmd","textlf"]},"text-symbol":{tokenType:"func",tokenValues:["textbackslash"]}};i.prototype._parseAtom=function(){for(var e in u){var t=u[e];var n=this._lexer.accept(t.tokenType,t.tokenValues);if(n===null)continue;var i=this._lexer.get().whitespace;if(e!=="ordinary"&&e!=="math")n=n.toLowerCase();return new o(e,n,i)}return null};t.exports=i},{"./ParseError":3,"./utils":6}],5:[function(n,e,t){var a=n("./utils");function A(e){this._css={};this._fontSize=this._outerFontSize=e!==undefined?e:1}A.prototype.outerFontSize=function(e){if(e!==undefined)this._outerFontSize=e;return this._outerFontSize};A.prototype.fontSize=function(){return this._fontSize};A.prototype._fontCommandTable={normalfont:{"font-family":"KaTeX_Main"},rmfamily:{"font-family":"KaTeX_Main"},sffamily:{"font-family":"KaTeX_SansSerif"},ttfamily:{"font-family":"KaTeX_Typewriter"},bfseries:{"font-weight":"bold"},mdseries:{"font-weight":"medium"},lfseries:{"font-weight":"lighter"},upshape:{"font-style":"normal","font-variant":"normal"},itshape:{"font-style":"italic","font-variant":"normal"},scshape:{"font-style":"normal","font-variant":"small-caps"},slshape:{"font-style":"oblique","font-variant":"normal"},textnormal:{"font-family":"KaTeX_Main"},textrm:{"font-family":"KaTeX_Main"},textsf:{"font-family":"KaTeX_SansSerif"},texttt:{"font-family":"KaTeX_Typewriter"},textbf:{"font-weight":"bold"},textmd:{"font-weight":"medium"},textlf:{"font-weight":"lighter"},textup:{"font-style":"normal","font-variant":"normal"},textit:{"font-style":"italic","font-variant":"normal"},textsc:{"font-style":"normal","font-variant":"small-caps"},textsl:{"font-style":"oblique","font-variant":"normal"},uppercase:{"text-transform":"uppercase"},lowercase:{"text-transform":"lowercase"}};A.prototype._sizingScalesTable={tiny:.68,scriptsize:.8,footnotesize:.85,small:.92,normalsize:1,large:1.17,Large:1.41,LARGE:1.58,huge:1.9,Huge:2.28};A.prototype.updateByCommand=function(e){var t=this._fontCommandTable[e];if(t!==undefined){for(var n in t)this._css[n]=t[n];return}var i=this._sizingScalesTable[e];if(i!==undefined){this._outerFontSize=this._fontSize;this._fontSize=i;return}throw new ParserError("Unrecognized `text-style` command")};A.prototype.toCSS=function(){var e="";for(var t in this._css){var n=this._css[t];if(n===undefined)continue;e+=`${t}:${n};`}if(this._fontSize!==this._outerFontSize)e+=`font-size:${this._fontSize/this._outerFontSize}em;`;return e};function B(e,t){this._nodes=e;this._textStyle=t}B.prototype._renderCloseText=function(e,t){var n=new A(this._textStyle.fontSize());var i=new B(e.children,n);if(e.whitespace)this._html.putText(" ");this._html.putHTML(i.renderToHTML(t))};B.prototype.renderToHTML=function(e){this._html=new _;var t;while((t=this._nodes.shift())!==undefined){var n=t.type;var i=t.value;if(t.whitespace)this._html.putText(" ");switch(n){case"ordinary":this._html.putText(i);break;case"math":if(typeof e==="undefined"){throw EvalError("No math backend found. Please setup KaTeX or MathJax.")}else if(e.name==="katex"){this._html.putHTML(e.driver.renderToString(i))}else if(e.name==="mathjax"){if(e.version<3){this._html.putText(`$${i}$`)}else{this._html.putHTML(e.driver.tex2chtml(i,{display:false}).outerHTML)}}else{throw new EvalError(`Unknown math backend ${e}`)}break;case"cond-symbol":this._html.beginSpan("ps-keyword").putText(i.toLowerCase()).endSpan();break;case"special":if(i==="\\\\"){this._html.putHTML("
");break}var r={"\\{":"{","\\}":"}","\\$":"$","\\&":"&","\\#":"#","\\%":"%","\\_":"_"};var o=r[i];this._html.putText(o);break;case"text-symbol":var s={textbackslash:"\\"};var a=s[i];this._html.putText(a);break;case"quote-symbol":var l={"`":"\u2018","``":"\u201c","'":"\u2019","''":"\u201d"};var h=l[i];this._html.putText(h);break;case"call":this._html.beginSpan("ps-funcname").putText(i).endSpan();this._html.write("(");var p=t.children[0];this._renderCloseText(p,e);this._html.write(")");break;case"close-text":this._renderCloseText(t,e);break;case"font-dclr":case"sizing-dclr":this._textStyle.updateByCommand(i);this._html.beginSpan(null,this._textStyle.toCSS());var u=new B(this._nodes,this._textStyle);this._html.putHTML(u.renderToHTML(e));this._html.endSpan();break;case"font-cmd":var c=this._nodes[0];if(c.type!=="close-text")continue;var f=new A(this._textStyle.fontSize());f.updateByCommand(i);this._html.beginSpan(null,f.toCSS());var d=new B(c.children,f);this._html.putHTML(d.renderToHTML(e));this._html.endSpan();break;default:throw new ParseError(`Unexpected ParseNode of type ${t.type}`)}}return this._html.toMarkup()};function _(){this._body=[];this._textBuf=[]}_.prototype.beginDiv=function(e,t,n){this._beginTag("div",e,t,n);this._body.push("\n");return this};_.prototype.endDiv=function(){this._endTag("div");this._body.push("\n");return this};_.prototype.beginP=function(e,t,n){this._beginTag("p",e,t,n);this._body.push("\n");return this};_.prototype.endP=function(){this._flushText();this._endTag("p");this._body.push("\n");return this};_.prototype.beginSpan=function(e,t,n){this._flushText();return this._beginTag("span",e,t,n)};_.prototype.endSpan=function(){this._flushText();return this._endTag("span")};_.prototype.putHTML=function(e){this._flushText();this._body.push(e);return this};_.prototype.putText=function(e){this._textBuf.push(e);return this};_.prototype.write=function(e){this._body.push(e)};_.prototype.toMarkup=function(){this._flushText();var e=this._body.join("");return e.trim()};_.prototype.toDOM=function(){var e=this.toMarkup();var t=document.createElement("div");t.innerHTML=e;return t.firstChild};_.prototype._flushText=function(){if(this._textBuf.length===0)return;var e=this._textBuf.join("");this._body.push(this._escapeHtml(e));this._textBuf=[]};_.prototype._beginTag=function(e,t,n,i){var r=`<${e}`;if(t)r+=` class="${t}"`;if(n){var o;if(a.isString(n)){o=n}else{o="";for(var s in n){attrVal=n[s];o+=`${s}:${attrVal};`}}if(i)o+=i;r+=` style="${o}"`}r+=">";this._body.push(r);return this};_.prototype._endTag=function(e){this._body.push(``);return this};var i={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};_.prototype._escapeHtml=function(e){return String(e).replace(/[&<>"'/]/g,e=>i[e])};function r(e){e=e||{};this.indentSize=e.indentSize?this._parseEmVal(e.indentSize):1.2;this.commentDelimiter=e.commentDelimiter!==undefined?e.commentDelimiter:" // ";this.lineNumberPunc=e.lineNumberPunc!==undefined?e.lineNumberPunc:":";this.lineNumber=e.lineNumber!==undefined?e.lineNumber:false;this.noEnd=e.noEnd!==undefined?e.noEnd:false;this.scopeLines=e.scopeLines!==undefined?e.scopeLines:false;if(e.captionCount!==undefined)F.captionCount=e.captionCount;this.titlePrefix=e.titlePrefix!==undefined?e.titlePrefix:"Algorithm"}r.prototype._parseEmVal=function(e){e=e.trim();if(e.indexOf("em")!==e.length-2)throw new TypeError("Unit error; expected `em` suffix");return Number(e.substring(0,e.length-2))};function F(e,t){this._root=e.parse();this._options=new r(t);this._openLine=false;this._blockLevel=0;this._textLevel=-1;this._globalTextStyle=new A;this.backend=undefined;try{if(typeof katex==="undefined")katex=n("katex")}catch(e){}try{if(typeof MathJax==="undefined")MathJax=n("mathjax")}catch(e){}if(typeof katex!=="undefined"){this.backend={name:"katex",driver:katex}}else if(typeof MathJax!=="undefined"){this.backend={name:"mathjax",version:parseInt(MathJax.version.split(".")[0]),driver:MathJax}}}F.captionCount=0;F.prototype.toMarkup=function(){var e=this._html=new _;this._buildTree(this._root);delete this._html;return e.toMarkup()};F.prototype.toDOM=function(){var e=this.toMarkup();var t=document.createElement("div");t.innerHTML=e;return t.firstChild};F.prototype._beginGroup=function(e,t,n){this._closeLineIfAny();this._html.beginDiv(`ps-${e}${t?` ${t}`:""}`,n)};F.prototype._endGroup=function(e){this._closeLineIfAny();this._html.endDiv()};F.prototype._beginBlock=function(){var e=this._options.lineNumber&&this._blockLevel===0?.6:0;var t=this._options.indentSize+e;if(this._options.scopeLines)t/=2;this._beginGroup("block",null,{"margin-left":`${t}em`});this._blockLevel++};F.prototype._endBlock=function(){this._closeLineIfAny();this._endGroup();this._blockLevel--};F.prototype._newLine=function(){this._closeLineIfAny();this._openLine=true;this._globalTextStyle.outerFontSize(1);var e=this._options.indentSize;if(this._blockLevel>0){this._numLOC++;this._html.beginP("ps-line ps-code",this._globalTextStyle.toCSS());var t=this._options.lineNumber?e*1.25:0;t+=this._options.scopeLines?e*.1:0;if(this._options.lineNumber){this._html.beginSpan("ps-linenum",{left:`${-((this._blockLevel-1)*t)}em`}).putText(this._numLOC+this._options.lineNumberPunc).endSpan()}}else{this._html.beginP("ps-line",{"text-indent":`${-e}em`,"padding-left":`${e}em`},this._globalTextStyle.toCSS())}};F.prototype._closeLineIfAny=function(){if(!this._openLine)return;this._html.endP();this._openLine=false};F.prototype._typeKeyword=function(e){this._html.beginSpan("ps-keyword").putText(e).endSpan()};F.prototype._typeFuncName=function(e){this._html.beginSpan("ps-funcname").putText(e).endSpan()};F.prototype._typeText=function(e){this._html.write(e)};F.prototype._buildTreeForAllChildren=function(e){var t=e.children;for(var n=0;n0&&t[0].type==="comment"){var n=t.shift();this._buildTree(n)}};F.prototype._buildTree=function(e){var t;var n;var i;switch(e.type){case"root":this._beginGroup("root");this._buildTreeForAllChildren(e);this._endGroup();break;case"algorithm":var r;for(t=0;t% arrange(neighbors, mtry)\n#> # A tibble: 6 × 2\n#> neighbors mtry\n#> \n#> 1 1 1\n#> 2 1 50\n#> 3 1 100\n#> 4 30 1\n#> 5 30 50\n#> 6 30 100\n```\n:::\n\n\n\nTo evaluate these candidates, we could just loop through each row, train a pipeline with that row’s candidate values, predictor a holdout set, and then compute a performance statistic. What does “train a pipeline” mean though? For the first candidate, it means \n\n- Carry out the UMAP estimation process on the training set using a single nearest neighbor. \n- Apply UMAP to the training set and save the transformed data. \n- Estimate a random forest model with `mtry = 1` using the transformed training set. \n\nThe first and third steps are two separate estimations.\n\nThe remaining tasks are to\n\n- Apply UMAP to the holdout data. \n- Predict the holdout data with the fitted random forest model. \n- Compute the performance statistic of interest (e.g., R2, accuracy, RMSE, etc.)\n\nFor the second candidate, the process is exactly the same _except_ that the random forest model uses `mtry = 50` instead of `mtry = 1`. \n\nWe _have_ to compute a new random forest model since it is a different model. However, the UMAP step is identical to the previous candidate’s preprocessor since it has the same values. Since UMAP is expensive, we end up spending a lot of time doing something that we have already done. \n\nThis would not be the case if there were no connection between the preprocessing tuning parameters and the model parameters, for example, this grid would not have repeated computations: \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```\n#> # A tibble: 6 × 2\n#> neighbors mtry\n#> \n#> 1 5 1\n#> 2 10 1\n#> 3 15 50\n#> 4 20 50\n#> 5 25 100\n#> 6 30 100\n```\n:::\n\n\n\nIn this case, the UMAP step would be different for each candidate and we would have to recompute it anyways. \n\nOnce solution to avoiding redundant computations would be to cache the UMAP computations so that they could be reused later. \n\nA better solutions is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. For the first grid of candidates contained in `umap_rf_grid`: \n\n:::: {.columns}\n\n::: {.column width=\"10%\"}\n\n:::\n\n::: {.column width=\"80%\"}\n\n\n```pseudocode\n#| html-line-number: true\n#| html-line-number-punc: \":\"\n\n\\begin{algorithm}\n\\begin{algorithmic}\n\\State $\\mathfrak{D}^{tr}$: training set\n\\State $\\mathfrak{D}^{ho}$: holdout set\n\\For{$k \\in \\{1, 30\\}$}\n \\State Using $k$ neighbors, train UMAP on $\\mathfrak{D}^{tr}$\n \\State Apply UMAP to $\\mathfrak{D}^{tr}$, creating $\\widehat{\\mathfrak{D}}^{tr}_k$\n \\State Apply UMAP to $\\mathfrak{D}^{ho}$, creating $\\widehat{\\mathfrak{D}}^{ho}_k$ \n \\For{$m \\in \\{1, 50, 100\\}$}\n \\State Train a random forest model with $m_{try} = m$ on $\\mathfrak{D}^{tr}_k$ to produce $\\widehat{f}_{km}$\n \\State Predict $\\widehat{\\mathfrak{D}}^{ho}_k$ with $\\widehat{f}_{km}$\n \\State Compute performance statistic $\\widehat{Q}_{km}$. \n \\EndFor\n\\EndFor\n\\State Determine the $k$ and $m$ calues corresponding to the best value of $\\widehat{Q}_{km}$.\n\\end{algorithmic}\n\\end{algorithm}\n```\n\n:::\n\n::: {.column width=\"10%\"}\n\n:::\n\n::::\n\n\nIn this way, we evaluated six candidates via two UMAP models and six random forest models. We avoid four redundant and expensive UMAP fits.\n\nWe can organize this data using a nested structure:\n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\numap_rf_nested_grid <- \n umap_rf_grid %>% \n group_nest(neighbors, .key = \"second_stage\")\numap_rf_nested_grid\n#> # A tibble: 2 × 2\n#> neighbors second_stage\n#> >\n#> 1 1 [3 × 1]\n#> 2 30 [3 × 1]\numap_rf_nested_grid$second_stage[[1]]\n#> # A tibble: 3 × 1\n#> mtry\n#> \n#> 1 1\n#> 2 50\n#> 3 100\n```\n:::\n\n\n\nIn general, this is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in section TODO when we can run the computations in parallel. \n\n## Sidebar: Types of Grids\n\nSince the type of grid mattered for this example, let’s go on a quick “side quest” to talk about the two major types of grids. \n\nthe effect of integers (max size etc) and also on duplicate computations\n\n## Sidebar: Parallel Processing\n\n\n\n## What are Submodels?\n\n\n## How Can we Exploit Submodels? \n\n\n## Types of Postprocessors\n\nThose needing tuning and those that are just applied. \n\n\n\n## Postprocessing Example: PRobability Threshold Optimization\n\n\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_quarto.yml b/_quarto.yml index 1a4d1bbf..c618e46e 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -14,6 +14,9 @@ execute: freeze: auto keep-md: true +filters: + - pseudocode + website: title: "tidymodels" navbar: @@ -105,10 +108,18 @@ format: quarto-required: ">= 1.3.353" toc: true linestretch: 1.6 - include-after-body: plausible.html + include-after-body: + - plausible.html + - MathJax.js grid: body-width: 840px +crossref: + custom: + - kind: float + reference-prefix: Algorithm + key: alg + theme: - cosmo - styles.scss diff --git a/contribute/index.html.md b/contribute/index.html.md index f24d593a..a97edef4 100644 --- a/contribute/index.html.md +++ b/contribute/index.html.md @@ -7,6 +7,8 @@ include-after-body: ../resources.html + + The ecosystem of tidymodels packages would not be possible without the contributions of the R community. No matter your current skills, it's possible to contribute back to tidymodels. Contributions are guided by our design goals. ## Design goals diff --git a/help/index.html.md b/help/index.html.md index 033cbd7d..76ec662e 100644 --- a/help/index.html.md +++ b/help/index.html.md @@ -7,6 +7,8 @@ include-after-body: ../resources.html + + ## Asking for help If you're asking for R help, reporting a bug, or requesting a new feature, you're more likely to succeed if you include a good reproducible example, which is precisely what the [reprex](https://reprex.tidyverse.org/) package is built for. You can learn more about reprex, along with other tips on how to help others help you in the [tidyverse.org help section](https://www.tidyverse.org/help/). diff --git a/learn/index-listing.json b/learn/index-listing.json index af434337..e01b34c4 100644 --- a/learn/index-listing.json +++ b/learn/index-listing.json @@ -16,6 +16,7 @@ "/learn/work/case-weights/index.html", "/learn/develop/metrics/index.html", "/learn/statistics/survival-metrics/index.html", + "/learn/work/efficient-grid-search/index.html", "/start/resampling/index.html", "/learn/work/fairness-readmission/index.html", "/learn/statistics/survival-case-study/index.html", diff --git a/learn/index.html.md b/learn/index.html.md index 14c13467..b6924ce6 100644 --- a/learn/index.html.md +++ b/learn/index.html.md @@ -18,5 +18,8 @@ listing: + + + After you know [what you need to get started](/start/) with tidymodels, you can learn more and go further. Find articles here to help you solve specific problems using the tidymodels framework. diff --git a/learn/models/sub-sampling/index.html.md b/learn/models/sub-sampling/index.html.md index 505b0ce7..f125c5d7 100644 --- a/learn/models/sub-sampling/index.html.md +++ b/learn/models/sub-sampling/index.html.md @@ -172,8 +172,8 @@ collect_metrics(qda_rose_res) #> # A tibble: 2 × 6 #> .metric .estimator mean n std_err .config #> -#> 1 j_index binary 0.768 50 0.0214 Preprocessor1_Model1 -#> 2 roc_auc binary 0.950 50 0.00494 Preprocessor1_Model1 +#> 1 j_index binary 0.769 50 0.0221 Preprocessor1_Model1 +#> 2 roc_auc binary 0.948 50 0.00498 Preprocessor1_Model1 ``` ::: diff --git a/learn/statistics/xtabs/index.html.md b/learn/statistics/xtabs/index.html.md index 7bdd7e3b..6f44d15f 100644 --- a/learn/statistics/xtabs/index.html.md +++ b/learn/statistics/xtabs/index.html.md @@ -192,12 +192,12 @@ p_value_independence #> # A tibble: 1 × 1 #> p_value #> -#> 1 0.0006 +#> 1 0.0004 ``` ::: -Thus, if there were really no relationship between cognition and genotype, the probability that we would see a statistic as or more extreme than 21.5774809 is approximately 6\times 10^{-4}. +Thus, if there were really no relationship between cognition and genotype, the probability that we would see a statistic as or more extreme than 21.5774809 is approximately 4\times 10^{-4}. Note that, equivalently to the steps shown above, the package supplies a wrapper function, `chisq_test`, to carry out Chi-Squared tests of independence on tidy data. The syntax goes like this: @@ -313,12 +313,12 @@ p_value_gof #> # A tibble: 1 × 1 #> p_value #> -#> 1 0.0006 +#> 1 0.0004 ``` ::: -Thus, if each genotype occurred at the same rate as the Song paper, the probability that we would see a distribution like the one we did is approximately 6\times 10^{-4}. +Thus, if each genotype occurred at the same rate as the Song paper, the probability that we would see a distribution like the one we did is approximately 4\times 10^{-4}. Again, equivalently to the steps shown above, the package supplies a wrapper function, `chisq_test`, to carry out chi-squared goodness of fit tests on tidy data. The syntax goes like this: diff --git a/learn/work/nested-resampling/index.html.md b/learn/work/nested-resampling/index.html.md index c399e24d..88992627 100644 --- a/learn/work/nested-resampling/index.html.md +++ b/learn/work/nested-resampling/index.html.md @@ -326,12 +326,12 @@ results <- summary(results$RMSE) #> Min. 1st Qu. Median Mean 3rd Qu. Max. -#> 1.574 2.095 2.645 2.696 3.252 4.350 +#> 1.574 2.095 2.673 2.687 3.258 4.325 ``` ::: -The estimated RMSE for the model tuning process is 2.7. +The estimated RMSE for the model tuning process is 2.69. What is the RMSE estimate for the non-nested procedure when only the outer resampling method is used? For each cost value in the tuning grid, 50 SVM models are fit and their RMSE values are averaged. The table of cost values and mean RMSE estimates is used to determine the best cost value. The associated RMSE is the biased estimate. diff --git a/site_libs/quarto-contrib/pseudocode-2.4.1/pseudocode.min.css b/site_libs/quarto-contrib/pseudocode-2.4.1/pseudocode.min.css new file mode 100644 index 00000000..c28ebfe4 --- /dev/null +++ b/site_libs/quarto-contrib/pseudocode-2.4.1/pseudocode.min.css @@ -0,0 +1 @@ +@import url(https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.css);.ps-root{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-size:1em;font-weight:100;-webkit-font-smoothing:antialiased!important}.ps-root .ps-algorithm{margin:.8em 0;border-top:3px solid #000;border-bottom:2px solid #000}.ps-root .ps-algorithm.with-caption>.ps-line:first-child{border-bottom:2px solid #000}.ps-root .katex{text-indent:0;font-size:1em}.ps-root .MathJax,.ps-root .MathJax_CHTML{text-indent:0;font-size:1em!important}.ps-root .ps-line{margin:0;padding:0;line-height:1.2}.ps-root .ps-funcname{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-weight:400;font-variant:small-caps;font-style:normal;text-transform:none}.ps-root .ps-keyword{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-weight:700;font-variant:normal;font-style:normal;text-transform:none}.ps-root .ps-comment{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-weight:400;font-variant:normal;font-style:normal;text-transform:none}.ps-root .ps-linenum{font-size:.8em;line-height:1em;width:1.6em;text-align:right;display:inline-block;position:relative;padding-right:.3em}.ps-root .ps-algorithmic.with-linenum .ps-line.ps-code{text-indent:-1.6em}.ps-root .ps-algorithmic.with-linenum .ps-line.ps-code>span{text-indent:0}.ps-root .ps-algorithmic.with-scopelines div.ps-block{border-left-style:solid;border-left-width:.1em;padding-left:.6em}.ps-root .ps-algorithmic.with-scopelines>div.ps-block{border-left:none} \ No newline at end of file diff --git a/site_libs/quarto-contrib/pseudocode-2.4.1/pseudocode.min.js b/site_libs/quarto-contrib/pseudocode-2.4.1/pseudocode.min.js new file mode 100644 index 00000000..e19fe9e0 --- /dev/null +++ b/site_libs/quarto-contrib/pseudocode-2.4.1/pseudocode.min.js @@ -0,0 +1 @@ +(function(e){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=e()}else if(typeof define==="function"&&define.amd){define([],e)}else{var t;if(typeof window!=="undefined"){t=window}else if(typeof global!=="undefined"){t=global}else if(typeof self!=="undefined"){t=self}else{t=this}t.pseudocode=e()}})(function(){var e,t,n;return function(){function p(o,s,a){function l(n,e){if(!s[n]){if(!o[n]){var t="function"==typeof require&&require;if(!e&&t)return t(n,!0);if(h)return h(n,!0);var i=new Error("Cannot find module '"+n+"'");throw i.code="MODULE_NOT_FOUND",i}var r=s[n]={exports:{}};o[n][0].call(r.exports,function(e){var t=o[n][1][e];return l(t||e)},r,r.exports,p,o,s,a)}return s[n].exports}for(var h="function"==typeof require&&require,e=0;ethis.renderElement(e,t))}}},{"./src/Lexer":2,"./src/ParseError":3,"./src/Parser":4,"./src/Renderer":5}],2:[function(e,t,n){var i=e("./utils");var u=e("./ParseError");var r=function(e){this._input=e;this._remain=e;this._pos=0;this._nextAtom=this._currentAtom=null;this._next()};r.prototype.accept=function(e,t){if(this._nextAtom.type===e&&this._matchText(t)){this._next();return this._currentAtom.text}return null};r.prototype.expect=function(e,t){var n=this._nextAtom;if(n.type!==e){throw new u(`Expected an atom of ${e} but received ${n.type}`,this._pos,this._input)}if(!this._matchText(t)){throw new u(`Expected \`${t}\` but received \`${n.text}\``,this._pos,this._input)}this._next();return this._currentAtom.text};r.prototype.get=function(){return this._currentAtom};var o={exec:function(e){var t=[{start:"$",end:"$"},{start:"\\(",end:"\\)"}];var n=e.length;for(var i=0;i0&&a[l-1]==="\\"){var h=l+o.length;a=a.slice(h);s+=h;continue}var p=[e.slice(0,s+l+o.length),e.slice(r.length,s+l)];return p}}return null}};var p={special:/^(\\\\|\\{|\\}|\\\$|\\&|\\#|\\%|\\_)/,math:o,func:/^\\([a-zA-Z]+)/,open:/^\{/,close:/^\}/,quote:/^(`|``|'|'')/,ordinary:/^[^\\{}$&#%_\s]+/};var c=/^%.*/;var f=/^\s+/;r.prototype._skip=function(e){this._pos+=e;this._remain=this._remain.slice(e)};r.prototype._next=function(){var e=false;while(1){var t=f.exec(this._remain);if(t){e=true;var n=t[0].length;this._skip(n)}var i=c.exec(this._remain);if(!i)break;var r=i[0].length;this._skip(r)}this._currentAtom=this._nextAtom;if(this._remain===""){this._nextAtom={type:"EOF",text:null,whitespace:false};return false}for(var o in p){var s=p[o];var a=s.exec(this._remain);if(!a)continue;var l=a[0];var h=a[1]?a[1]:l;this._nextAtom={type:o,text:h,whitespace:e};this._pos+=l.length;this._remain=this._remain.slice(a[0].length);return true}throw new u("Unrecoganizable atom",this._pos,this._input)};r.prototype._matchText=function(e){if(e===null||e===undefined)return true;if(i.isString(e))return e.toLowerCase()===this._nextAtom.text.toLowerCase();else return e.some(e=>e.toLowerCase()===this._nextAtom.text.toLowerCase())};t.exports=r},{"./ParseError":3,"./utils":6}],3:[function(e,t,n){function i(e,t,n){var i=`Error: ${e}`;if(t!==undefined&&n!==undefined){i+=` at position ${t}: \``;n=`${n.slice(0,t)}\u21B1${n.slice(t)}`;var r=Math.max(0,t-15);var o=t+15;i+=`${n.slice(r,o)}\``}this.message=i}i.prototype=Object.create(Error.prototype);i.prototype.constructor=i;t.exports=i},{}],4:[function(e,t,n){var s=e("./utils");var r=e("./ParseError");var a=function(e,t){this.type=e;this.value=t;this.children=[]};a.prototype.toString=function(e){if(!e)e=0;var t="";for(var n=0;n`;if(this.value)i+=` (${s.toString(this.value)})`;i+="\n";if(this.children){for(var r=0;r0){e.addChild(t);continue}break}return e};i.prototype._parseCaption=function(){var e=this._lexer;if(!e.accept("func","caption"))return null;var t=new a("caption");e.expect("open");t.addChild(this._parseCloseText());e.expect("close");return t};i.prototype._parseBlock=function(){var e=new a("block");while(true){var t=this._parseControl();if(t){e.addChild(t);continue}var n=this._parseFunction();if(n){e.addChild(n);continue}var i=this._parseStatement(h);if(i){e.addChild(i);continue}var r=this._parseCommand(p);if(r){e.addChild(r);continue}var o=this._parseComment();if(o){e.addChild(o);continue}break}return e};i.prototype._parseControl=function(){var e;if(e=this._parseIf())return e;if(e=this._parseLoop())return e;if(e=this._parseRepeat())return e;if(e=this._parseUpon())return e};i.prototype._parseFunction=function(){var e=this._lexer;if(!e.accept("func",["function","procedure"]))return null;var t=this._lexer.get().text;e.expect("open");var n=e.expect("ordinary");e.expect("close");e.expect("open");var i=this._parseCloseText();e.expect("close");var r=this._parseBlock();e.expect("func",`end${t}`);var o=new a("function",{type:t,name:n});o.addChild(i);o.addChild(r);return o};i.prototype._parseIf=function(){if(!this._lexer.accept("func","if"))return null;var e=new a("if");this._lexer.expect("open");e.addChild(this._parseCond());this._lexer.expect("close");e.addChild(this._parseBlock());var t=0;while(this._lexer.accept("func",["elif","elsif","elseif"])){this._lexer.expect("open");e.addChild(this._parseCond());this._lexer.expect("close");e.addChild(this._parseBlock());t++}var n=false;if(this._lexer.accept("func","else")){n=true;e.addChild(this._parseBlock())}this._lexer.expect("func","endif");e.value={numElif:t,hasElse:n};return e};i.prototype._parseLoop=function(){if(!this._lexer.accept("func",["FOR","FORALL","WHILE"]))return null;var e=this._lexer.get().text.toLowerCase();var t=new a("loop",e);this._lexer.expect("open");t.addChild(this._parseCond());this._lexer.expect("close");t.addChild(this._parseBlock());var n=e!=="forall"?`end${e}`:"endfor";this._lexer.expect("func",n);return t};i.prototype._parseRepeat=function(){if(!this._lexer.accept("func",["REPEAT"]))return null;var e=this._lexer.get().text.toLowerCase();var t=new a("repeat",e);t.addChild(this._parseBlock());this._lexer.expect("func","until");this._lexer.expect("open");t.addChild(this._parseCond());this._lexer.expect("close");return t};i.prototype._parseUpon=function(){if(!this._lexer.accept("func","upon"))return null;var e=new a("upon");this._lexer.expect("open");e.addChild(this._parseCond());this._lexer.expect("close");e.addChild(this._parseBlock());this._lexer.expect("func","endupon");return e};var l=["ensure","require","input","output"];var h=["state","print","return"];i.prototype._parseStatement=function(e){if(!this._lexer.accept("func",e))return null;var t=this._lexer.get().text.toLowerCase();var n=new a("statement",t);n.addChild(this._parseOpenText());return n};var p=["break","continue"];i.prototype._parseCommand=function(e){if(!this._lexer.accept("func",e))return null;var t=this._lexer.get().text.toLowerCase();var n=new a("command",t);return n};i.prototype._parseComment=function(){if(!this._lexer.accept("func","comment"))return null;var e=new a("comment");this._lexer.expect("open");e.addChild(this._parseCloseText());this._lexer.expect("close");return e};i.prototype._parseCall=function(){var e=this._lexer;if(!e.accept("func","call"))return null;var t=e.get().whitespace;e.expect("open");var n=e.expect("ordinary");e.expect("close");var i=new a("call");i.whitespace=t;i.value=n;e.expect("open");var r=this._parseCloseText();i.addChild(r);e.expect("close");return i};i.prototype._parseCond=i.prototype._parseCloseText=function(){return this._parseText("close")};i.prototype._parseOpenText=function(){return this._parseText("open")};i.prototype._parseText=function(e){var t=new a(`${e}-text`);var n=false;var i;while(true){i=this._parseAtom()||this._parseCall();if(i){if(n)i.whitespace|=n;t.addChild(i);continue}if(this._lexer.accept("open")){i=this._parseCloseText();n=this._lexer.get().whitespace;i.whitespace=n;t.addChild(i);this._lexer.expect("close");n=this._lexer.get().whitespace;continue}break}return t};var u={ordinary:{tokenType:"ordinary"},math:{tokenType:"math"},special:{tokenType:"special"},"cond-symbol":{tokenType:"func",tokenValues:["and","or","not","true","false","to","downto"]},"quote-symbol":{tokenType:"quote"},"sizing-dclr":{tokenType:"func",tokenValues:["tiny","scriptsize","footnotesize","small","normalsize","large","Large","LARGE","huge","Huge"]},"font-dclr":{tokenType:"func",tokenValues:["normalfont","rmfamily","sffamily","ttfamily","upshape","itshape","slshape","scshape","bfseries","mdseries","lfseries"]},"font-cmd":{tokenType:"func",tokenValues:["textnormal","textrm","textsf","texttt","textup","textit","textsl","textsc","uppercase","lowercase","textbf","textmd","textlf"]},"text-symbol":{tokenType:"func",tokenValues:["textbackslash"]}};i.prototype._parseAtom=function(){for(var e in u){var t=u[e];var n=this._lexer.accept(t.tokenType,t.tokenValues);if(n===null)continue;var i=this._lexer.get().whitespace;if(e!=="ordinary"&&e!=="math")n=n.toLowerCase();return new o(e,n,i)}return null};t.exports=i},{"./ParseError":3,"./utils":6}],5:[function(n,e,t){var a=n("./utils");function A(e){this._css={};this._fontSize=this._outerFontSize=e!==undefined?e:1}A.prototype.outerFontSize=function(e){if(e!==undefined)this._outerFontSize=e;return this._outerFontSize};A.prototype.fontSize=function(){return this._fontSize};A.prototype._fontCommandTable={normalfont:{"font-family":"KaTeX_Main"},rmfamily:{"font-family":"KaTeX_Main"},sffamily:{"font-family":"KaTeX_SansSerif"},ttfamily:{"font-family":"KaTeX_Typewriter"},bfseries:{"font-weight":"bold"},mdseries:{"font-weight":"medium"},lfseries:{"font-weight":"lighter"},upshape:{"font-style":"normal","font-variant":"normal"},itshape:{"font-style":"italic","font-variant":"normal"},scshape:{"font-style":"normal","font-variant":"small-caps"},slshape:{"font-style":"oblique","font-variant":"normal"},textnormal:{"font-family":"KaTeX_Main"},textrm:{"font-family":"KaTeX_Main"},textsf:{"font-family":"KaTeX_SansSerif"},texttt:{"font-family":"KaTeX_Typewriter"},textbf:{"font-weight":"bold"},textmd:{"font-weight":"medium"},textlf:{"font-weight":"lighter"},textup:{"font-style":"normal","font-variant":"normal"},textit:{"font-style":"italic","font-variant":"normal"},textsc:{"font-style":"normal","font-variant":"small-caps"},textsl:{"font-style":"oblique","font-variant":"normal"},uppercase:{"text-transform":"uppercase"},lowercase:{"text-transform":"lowercase"}};A.prototype._sizingScalesTable={tiny:.68,scriptsize:.8,footnotesize:.85,small:.92,normalsize:1,large:1.17,Large:1.41,LARGE:1.58,huge:1.9,Huge:2.28};A.prototype.updateByCommand=function(e){var t=this._fontCommandTable[e];if(t!==undefined){for(var n in t)this._css[n]=t[n];return}var i=this._sizingScalesTable[e];if(i!==undefined){this._outerFontSize=this._fontSize;this._fontSize=i;return}throw new ParserError("Unrecognized `text-style` command")};A.prototype.toCSS=function(){var e="";for(var t in this._css){var n=this._css[t];if(n===undefined)continue;e+=`${t}:${n};`}if(this._fontSize!==this._outerFontSize)e+=`font-size:${this._fontSize/this._outerFontSize}em;`;return e};function B(e,t){this._nodes=e;this._textStyle=t}B.prototype._renderCloseText=function(e,t){var n=new A(this._textStyle.fontSize());var i=new B(e.children,n);if(e.whitespace)this._html.putText(" ");this._html.putHTML(i.renderToHTML(t))};B.prototype.renderToHTML=function(e){this._html=new _;var t;while((t=this._nodes.shift())!==undefined){var n=t.type;var i=t.value;if(t.whitespace)this._html.putText(" ");switch(n){case"ordinary":this._html.putText(i);break;case"math":if(typeof e==="undefined"){throw EvalError("No math backend found. Please setup KaTeX or MathJax.")}else if(e.name==="katex"){this._html.putHTML(e.driver.renderToString(i))}else if(e.name==="mathjax"){if(e.version<3){this._html.putText(`$${i}$`)}else{this._html.putHTML(e.driver.tex2chtml(i,{display:false}).outerHTML)}}else{throw new EvalError(`Unknown math backend ${e}`)}break;case"cond-symbol":this._html.beginSpan("ps-keyword").putText(i.toLowerCase()).endSpan();break;case"special":if(i==="\\\\"){this._html.putHTML("
");break}var r={"\\{":"{","\\}":"}","\\$":"$","\\&":"&","\\#":"#","\\%":"%","\\_":"_"};var o=r[i];this._html.putText(o);break;case"text-symbol":var s={textbackslash:"\\"};var a=s[i];this._html.putText(a);break;case"quote-symbol":var l={"`":"\u2018","``":"\u201c","'":"\u2019","''":"\u201d"};var h=l[i];this._html.putText(h);break;case"call":this._html.beginSpan("ps-funcname").putText(i).endSpan();this._html.write("(");var p=t.children[0];this._renderCloseText(p,e);this._html.write(")");break;case"close-text":this._renderCloseText(t,e);break;case"font-dclr":case"sizing-dclr":this._textStyle.updateByCommand(i);this._html.beginSpan(null,this._textStyle.toCSS());var u=new B(this._nodes,this._textStyle);this._html.putHTML(u.renderToHTML(e));this._html.endSpan();break;case"font-cmd":var c=this._nodes[0];if(c.type!=="close-text")continue;var f=new A(this._textStyle.fontSize());f.updateByCommand(i);this._html.beginSpan(null,f.toCSS());var d=new B(c.children,f);this._html.putHTML(d.renderToHTML(e));this._html.endSpan();break;default:throw new ParseError(`Unexpected ParseNode of type ${t.type}`)}}return this._html.toMarkup()};function _(){this._body=[];this._textBuf=[]}_.prototype.beginDiv=function(e,t,n){this._beginTag("div",e,t,n);this._body.push("\n");return this};_.prototype.endDiv=function(){this._endTag("div");this._body.push("\n");return this};_.prototype.beginP=function(e,t,n){this._beginTag("p",e,t,n);this._body.push("\n");return this};_.prototype.endP=function(){this._flushText();this._endTag("p");this._body.push("\n");return this};_.prototype.beginSpan=function(e,t,n){this._flushText();return this._beginTag("span",e,t,n)};_.prototype.endSpan=function(){this._flushText();return this._endTag("span")};_.prototype.putHTML=function(e){this._flushText();this._body.push(e);return this};_.prototype.putText=function(e){this._textBuf.push(e);return this};_.prototype.write=function(e){this._body.push(e)};_.prototype.toMarkup=function(){this._flushText();var e=this._body.join("");return e.trim()};_.prototype.toDOM=function(){var e=this.toMarkup();var t=document.createElement("div");t.innerHTML=e;return t.firstChild};_.prototype._flushText=function(){if(this._textBuf.length===0)return;var e=this._textBuf.join("");this._body.push(this._escapeHtml(e));this._textBuf=[]};_.prototype._beginTag=function(e,t,n,i){var r=`<${e}`;if(t)r+=` class="${t}"`;if(n){var o;if(a.isString(n)){o=n}else{o="";for(var s in n){attrVal=n[s];o+=`${s}:${attrVal};`}}if(i)o+=i;r+=` style="${o}"`}r+=">";this._body.push(r);return this};_.prototype._endTag=function(e){this._body.push(``);return this};var i={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};_.prototype._escapeHtml=function(e){return String(e).replace(/[&<>"'/]/g,e=>i[e])};function r(e){e=e||{};this.indentSize=e.indentSize?this._parseEmVal(e.indentSize):1.2;this.commentDelimiter=e.commentDelimiter!==undefined?e.commentDelimiter:" // ";this.lineNumberPunc=e.lineNumberPunc!==undefined?e.lineNumberPunc:":";this.lineNumber=e.lineNumber!==undefined?e.lineNumber:false;this.noEnd=e.noEnd!==undefined?e.noEnd:false;this.scopeLines=e.scopeLines!==undefined?e.scopeLines:false;if(e.captionCount!==undefined)F.captionCount=e.captionCount;this.titlePrefix=e.titlePrefix!==undefined?e.titlePrefix:"Algorithm"}r.prototype._parseEmVal=function(e){e=e.trim();if(e.indexOf("em")!==e.length-2)throw new TypeError("Unit error; expected `em` suffix");return Number(e.substring(0,e.length-2))};function F(e,t){this._root=e.parse();this._options=new r(t);this._openLine=false;this._blockLevel=0;this._textLevel=-1;this._globalTextStyle=new A;this.backend=undefined;try{if(typeof katex==="undefined")katex=n("katex")}catch(e){}try{if(typeof MathJax==="undefined")MathJax=n("mathjax")}catch(e){}if(typeof katex!=="undefined"){this.backend={name:"katex",driver:katex}}else if(typeof MathJax!=="undefined"){this.backend={name:"mathjax",version:parseInt(MathJax.version.split(".")[0]),driver:MathJax}}}F.captionCount=0;F.prototype.toMarkup=function(){var e=this._html=new _;this._buildTree(this._root);delete this._html;return e.toMarkup()};F.prototype.toDOM=function(){var e=this.toMarkup();var t=document.createElement("div");t.innerHTML=e;return t.firstChild};F.prototype._beginGroup=function(e,t,n){this._closeLineIfAny();this._html.beginDiv(`ps-${e}${t?` ${t}`:""}`,n)};F.prototype._endGroup=function(e){this._closeLineIfAny();this._html.endDiv()};F.prototype._beginBlock=function(){var e=this._options.lineNumber&&this._blockLevel===0?.6:0;var t=this._options.indentSize+e;if(this._options.scopeLines)t/=2;this._beginGroup("block",null,{"margin-left":`${t}em`});this._blockLevel++};F.prototype._endBlock=function(){this._closeLineIfAny();this._endGroup();this._blockLevel--};F.prototype._newLine=function(){this._closeLineIfAny();this._openLine=true;this._globalTextStyle.outerFontSize(1);var e=this._options.indentSize;if(this._blockLevel>0){this._numLOC++;this._html.beginP("ps-line ps-code",this._globalTextStyle.toCSS());var t=this._options.lineNumber?e*1.25:0;t+=this._options.scopeLines?e*.1:0;if(this._options.lineNumber){this._html.beginSpan("ps-linenum",{left:`${-((this._blockLevel-1)*t)}em`}).putText(this._numLOC+this._options.lineNumberPunc).endSpan()}}else{this._html.beginP("ps-line",{"text-indent":`${-e}em`,"padding-left":`${e}em`},this._globalTextStyle.toCSS())}};F.prototype._closeLineIfAny=function(){if(!this._openLine)return;this._html.endP();this._openLine=false};F.prototype._typeKeyword=function(e){this._html.beginSpan("ps-keyword").putText(e).endSpan()};F.prototype._typeFuncName=function(e){this._html.beginSpan("ps-funcname").putText(e).endSpan()};F.prototype._typeText=function(e){this._html.write(e)};F.prototype._buildTreeForAllChildren=function(e){var t=e.children;for(var n=0;n0&&t[0].type==="comment"){var n=t.shift();this._buildTree(n)}};F.prototype._buildTree=function(e){var t;var n;var i;switch(e.type){case"root":this._beginGroup("root");this._buildTreeForAllChildren(e);this._endGroup();break;case"algorithm":var r;for(t=0;t { // Find any conflicting margin elements and add margins to the // top to prevent overlap const marginChildren = window.document.querySelectorAll( - ".column-margin.column-container > * " + ".column-margin.column-container > *, .margin-caption, .aside" ); let lastBottom = 0; @@ -19,7 +19,9 @@ const layoutMarginEls = () => { marginChild.style.marginTop = null; const top = marginChild.getBoundingClientRect().top + window.scrollY; if (top < lastBottom) { - const margin = lastBottom - top; + const marginChildStyle = window.getComputedStyle(marginChild); + const marginBottom = parseFloat(marginChildStyle["marginBottom"]); + const margin = lastBottom - top + marginBottom; marginChild.style.marginTop = `${margin}px`; } const styles = window.getComputedStyle(marginChild); @@ -92,7 +94,7 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { if (link.href.indexOf("#") !== -1) { const anchor = link.href.split("#")[1]; const heading = window.document.querySelector( - `[data-anchor-id=${anchor}]` + `[data-anchor-id="${anchor}"]` ); if (heading) { // Add the class @@ -132,8 +134,10 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { window.innerHeight + window.pageYOffset >= window.document.body.offsetHeight ) { + // This is the no-scroll case where last section should be the active one sectionIndex = 0; } else { + // This finds the last section visible on screen that should be made active sectionIndex = [...sections].reverse().findIndex((section) => { if (section) { return window.pageYOffset >= section.offsetTop - sectionMargin; @@ -315,6 +319,7 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { for (const child of el.children) { child.style.opacity = 0; child.style.overflow = "hidden"; + child.style.pointerEvents = "none"; } nexttick(() => { @@ -356,6 +361,7 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { const clone = child.cloneNode(true); clone.style.opacity = 1; + clone.style.pointerEvents = null; clone.style.display = null; toggleContents.append(clone); } @@ -430,6 +436,7 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { for (const child of el.children) { child.style.opacity = 1; child.style.overflow = null; + child.style.pointerEvents = null; } const placeholderEl = window.document.getElementById( @@ -737,6 +744,7 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { // Process the collapse state if this is an UL if (el.tagName === "UL") { if (tocOpenDepth === -1 && depth > 1) { + // toc-expand: false el.classList.add("collapse"); } else if ( depth <= tocOpenDepth || @@ -755,10 +763,9 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { }; // walk the TOC and expand / collapse any items that should be shown - if (tocEl) { - walk(tocEl, 0); updateActiveLink(); + walk(tocEl, 0); } // Throttle the scroll event and walk peridiocally @@ -777,6 +784,10 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { window.addEventListener( "resize", throttle(() => { + if (tocEl) { + updateActiveLink(); + walk(tocEl, 0); + } if (!isReaderMode()) { hideOverlappedSidebars(); } diff --git a/site_libs/quarto-nav/quarto-nav.js b/site_libs/quarto-nav/quarto-nav.js index 1eb2941f..38cc4305 100644 --- a/site_libs/quarto-nav/quarto-nav.js +++ b/site_libs/quarto-nav/quarto-nav.js @@ -273,6 +273,7 @@ window.document.addEventListener("DOMContentLoaded", function () { const links = window.document.querySelectorAll("a"); for (let i = 0; i < links.length; i++) { if (links[i].href) { + links[i].dataset.originalHref = links[i].href; links[i].href = links[i].href.replace(/\/index\.html/, "/"); } } diff --git a/site_libs/quarto-search/quarto-search.js b/site_libs/quarto-search/quarto-search.js index b54e65fc..d788a958 100644 --- a/site_libs/quarto-search/quarto-search.js +++ b/site_libs/quarto-search/quarto-search.js @@ -393,7 +393,12 @@ window.document.addEventListener("DOMContentLoaded", function (_event) { return focusedEl.tagName.toLowerCase() === tag; }); - if (kbds && kbds.includes(key) && !isFormElFocused) { + if ( + kbds && + kbds.includes(key) && + !isFormElFocused && + !document.activeElement.isContentEditable + ) { event.preventDefault(); window.quartoOpenSearch(); } @@ -670,6 +675,18 @@ function showCopyLink(query, options) { // create the index var fuseIndex = undefined; var shownWarning = false; + +// fuse index options +const kFuseIndexOptions = { + keys: [ + { name: "title", weight: 20 }, + { name: "section", weight: 20 }, + { name: "text", weight: 10 }, + ], + ignoreLocation: true, + threshold: 0.1, +}; + async function readSearchData() { // Initialize the search index on demand if (fuseIndex === undefined) { @@ -680,17 +697,7 @@ async function readSearchData() { shownWarning = true; return; } - // create fuse index - const options = { - keys: [ - { name: "title", weight: 20 }, - { name: "section", weight: 20 }, - { name: "text", weight: 10 }, - ], - ignoreLocation: true, - threshold: 0.1, - }; - const fuse = new window.Fuse([], options); + const fuse = new window.Fuse([], kFuseIndexOptions); // fetch the main search.json const response = await fetch(offsetURL("search.json")); @@ -1221,8 +1228,34 @@ function algoliaSearch(query, limit, algoliaOptions) { }); } -function fuseSearch(query, fuse, fuseOptions) { - return fuse.search(query, fuseOptions).map((result) => { +let subSearchTerm = undefined; +let subSearchFuse = undefined; +const kFuseMaxWait = 125; + +async function fuseSearch(query, fuse, fuseOptions) { + let index = fuse; + // Fuse.js using the Bitap algorithm for text matching which runs in + // O(nm) time (no matter the structure of the text). In our case this + // means that long search terms mixed with large index gets very slow + // + // This injects a subIndex that will be used once the terms get long enough + // Usually making this subindex is cheap since there will typically be + // a subset of results matching the existing query + if (subSearchFuse !== undefined && query.startsWith(subSearchTerm)) { + // Use the existing subSearchFuse + index = subSearchFuse; + } else if (subSearchFuse !== undefined) { + // The term changed, discard the existing fuse + subSearchFuse = undefined; + subSearchTerm = undefined; + } + + // Search using the active fuse + const then = performance.now(); + const resultsRaw = await index.search(query, fuseOptions); + const now = performance.now(); + + const results = resultsRaw.map((result) => { const addParam = (url, name, value) => { const anchorParts = url.split("#"); const baseUrl = anchorParts[0]; @@ -1239,4 +1272,19 @@ function fuseSearch(query, fuse, fuseOptions) { crumbs: result.item.crumbs, }; }); + + // If we don't have a subfuse and the query is long enough, go ahead + // and create a subfuse to use for subsequent queries + if ( + now - then > kFuseMaxWait && + subSearchFuse === undefined && + resultsRaw.length < fuseOptions.limit + ) { + subSearchTerm = query; + subSearchFuse = new window.Fuse([], kFuseIndexOptions); + resultsRaw.forEach((rr) => { + subSearchFuse.add(rr.item); + }); + } + return results; } From 247b4d4fe6c4f2743343a454b703012abe727f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98topepo=E2=80=99?= Date: Sun, 24 Nov 2024 16:55:07 -0500 Subject: [PATCH 02/13] grid search start --- .../work/efficient-grid-search/index.html.md | 237 ++++++++++++++++++ learn/work/efficient-grid-search/index.qmd | 200 +++++++++++++++ 2 files changed, 437 insertions(+) create mode 100644 learn/work/efficient-grid-search/index.html.md create mode 100644 learn/work/efficient-grid-search/index.qmd diff --git a/learn/work/efficient-grid-search/index.html.md b/learn/work/efficient-grid-search/index.html.md new file mode 100644 index 00000000..c044012f --- /dev/null +++ b/learn/work/efficient-grid-search/index.html.md @@ -0,0 +1,237 @@ +--- +title: "Efficient Grid Search" +categories: + - tuning +type: learn-subsection +weight: 4 +description: | + A general discussion on grids and how to efficiently estimate performance. . +toc: true +toc-depth: 2 +include-after-body: ../../../resources.html +--- + + + + + + +## Introduction + +To use code in this article, you will need to install the following packages: tidymodels. + +This article demonstrates .... + + +## The Overall Grid Tuning Procedure + +tidymodels has three phases of creating a model pipeline: + + - preprocessors: computations that prepare data for the model + - supervised model: the actual model fit + - postprocessing: adjustments to model predictions + +Each of these stages can have tuning parameters. Some examples: + +- We might add a spline basis expansion to a predictor to enable to have a nonlinear trend in the model. We take one predictor and create multiple columns that are based on the original column. As we add new spline columns, the model can be more complex. We don't know how many columns to add so we try different values and see which maximizes performance. + +- If many predictors are correlated with one another, we could determine which predictors (and how many) can be removed to reduce multicollinearity. One approach is the have a threshold for the maximum allowable pairwise correlations and have an algorithm remove the smallest set of predictors to satisfy the constraint. The threshold would need to be tuned. + +- Most models have tuning parameters. Boosted trees have many but two of the main parameters are the learning rate and the number of trees in the ensemble. Both usually require tuning. + +- For binary classification models, especially those with a class imbalance, it is possible that the default 50% probability threshold that is used to define what is "an event" does not satisfy the user's needs. For example, when screening blood bank samples, we want a model to err on the side of throwing out disease-free donations as long as we minimize the number of bad blood samples that truly are diseased. In other words, we might want to tune the probability threshold to achieve a high specificity metric. + +Once we know what the tuning parameters are and which phases of the pipeline they are from, we can create a grid of values to evaluate. Since a pipeline can have multiple tuning parameters, a _candidate_ is defined to be a single set of actual parameter values that could be tested. A set of multiple tuning parameter candidate values is a grid. + +For grid search, we evaluate each candidate in the grid by training the entire pipeline, predicting a set of data, and computing performance metrics that estimate the candidate’s efficacy. Once this is finished, we can pick the candidate set that shows the best value. + +This page outlines the general process and describes different constraints and approaches to efficiently evaluating a grid of candidate values. Unexpectedly, the tools that we use to increase efficiency often result in more and more complex routines to compute performance for the grid. + +We’ll break down some of the complications and exploitations that are relevant for model tuning (via grid search). We’ll also start with very simple use-cases and steadily increase the complexity of the task as well as the complexity of the solutions. + +## How Does Training Occur in the Pipeline? + +We should describe how different models would accomplish the three stages of a model pipeline. + +On one hand, there are deep neural networks. These are highly nonlinear models whose model structure is defined as a sequence of layers. The architecture of the network is defined by different types of layers that are intended to do different things. The mantra of deep learning is to add layers to the network that can accomplish all of the tasks discussed above. There is little to no delineation regarding what is a pre- or post-processor; it is all part of the model and all parameters are estimated simultaneously. We will call this type of model a “simultaneous estimation” since it all happens at the same time. + +On the other hand is nearly every other type of model. In most cases, preprocessing tasks are separate estimation procedures that are executed independently of the model fit. There are cases such as principal component regression that conducts PCA signal extraction before passing the results to a routine that executes ordinary least squares. However, there are still two separate estimation tasks that are carried out in sequence, not simultaneously. We’ll call this type of training “stagewise estimation” since there are multiple, distinct training steps that occur in sequence. + +Our focus is 100% on stagewise estimation. + +## Stage-Wise Modeling and Conditional Execution + +One important part of processing a grid of candidate values is to avoid repeating any computations wherever possible. This leads to the idea of conditional execution. Let’s think of an example. + +Suppose our pipeline consists of one preprocess and the model fit (i.e., no postprocessing). Let’s choose a fairly expensive preprocessing technique: UMAP feature extraction. UMAP has a variety of tuning parameters and one is the number of nearest neighbors to use when creating a network of nearby training set samples. Suppose that we will consider values ranging from 1 to 30. + +For our model, we’ll choose a random forest model. This also has tuning parameters and we’ll choose to optimize “m-try”; the number of predictors to randomly select each time a split is created in any decision tree. + +For illustration, let’s use a grid of points with six candidates: + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +library(tidymodels) +umap_rf_param <- parameters(neighbors(c(1, 30)), mtry(c(1, 100))) +umap_rf_grid <- grid_regular(umap_rf_param, levels = c(2, 3)) +umap_rf_grid %>% arrange(neighbors, mtry) +#> # A tibble: 6 × 2 +#> neighbors mtry +#> +#> 1 1 1 +#> 2 1 50 +#> 3 1 100 +#> 4 30 1 +#> 5 30 50 +#> 6 30 100 +``` +::: + + + +To evaluate these candidates, we could just loop through each row, train a pipeline with that row’s candidate values, predictor a holdout set, and then compute a performance statistic. What does “train a pipeline” mean though? For the first candidate, it means + +- Carry out the UMAP estimation process on the training set using a single nearest neighbor. +- Apply UMAP to the training set and save the transformed data. +- Estimate a random forest model with `mtry = 1` using the transformed training set. + +The first and third steps are two separate estimations. + +The remaining tasks are to + +- Apply UMAP to the holdout data. +- Predict the holdout data with the fitted random forest model. +- Compute the performance statistic of interest (e.g., R2, accuracy, RMSE, etc.) + +For the second candidate, the process is exactly the same _except_ that the random forest model uses `mtry = 50` instead of `mtry = 1`. + +We _have_ to compute a new random forest model since it is a different model. However, the UMAP step is identical to the previous candidate’s preprocessor since it has the same values. Since UMAP is expensive, we end up spending a lot of time doing something that we have already done. + +This would not be the case if there were no connection between the preprocessing tuning parameters and the model parameters, for example, this grid would not have repeated computations: + + + +::: {.cell layout-align="center"} + +``` +#> # A tibble: 6 × 2 +#> neighbors mtry +#> +#> 1 5 1 +#> 2 10 1 +#> 3 15 50 +#> 4 20 50 +#> 5 25 100 +#> 6 30 100 +``` +::: + + + +In this case, the UMAP step would be different for each candidate and we would have to recompute it anyways. + +Once solution to avoiding redundant computations would be to cache the UMAP computations so that they could be reused later. + +A better solutions is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. For the first grid of candidates contained in `umap_rf_grid`: + +:::: {.columns} + +::: {.column width="10%"} + +::: + +::: {.column width="80%"} + + +```pseudocode +#| html-line-number: true +#| html-line-number-punc: ":" + +\begin{algorithm} +\begin{algorithmic} +\State $\mathfrak{D}^{tr}$: training set +\State $\mathfrak{D}^{ho}$: holdout set +\For{$k \in \{1, 30\}$} + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{tr}$ + \State Apply UMAP to $\mathfrak{D}^{tr}$, creating $\widehat{\mathfrak{D}}^{tr}_k$ + \State Apply UMAP to $\mathfrak{D}^{ho}$, creating $\widehat{\mathfrak{D}}^{ho}_k$ + \For{$m \in \{1, 50, 100\}$} + \State Train a random forest model with $m_{try} = m$ on $\mathfrak{D}^{tr}_k$ to produce $\widehat{f}_{km}$ + \State Predict $\widehat{\mathfrak{D}}^{ho}_k$ with $\widehat{f}_{km}$ + \State Compute performance statistic $\widehat{Q}_{km}$. + \EndFor +\EndFor +\State Determine the $k$ and $m$ calues corresponding to the best value of $\widehat{Q}_{km}$. +\end{algorithmic} +\end{algorithm} +``` + +::: + +::: {.column width="10%"} + +::: + +:::: + + +In this way, we evaluated six candidates via two UMAP models and six random forest models. We avoid four redundant and expensive UMAP fits. + +We can organize this data using a nested structure: + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +umap_rf_nested_grid <- + umap_rf_grid %>% + group_nest(neighbors, .key = "second_stage") +umap_rf_nested_grid +#> # A tibble: 2 × 2 +#> neighbors second_stage +#> > +#> 1 1 [3 × 1] +#> 2 30 [3 × 1] +umap_rf_nested_grid$second_stage[[1]] +#> # A tibble: 3 × 1 +#> mtry +#> +#> 1 1 +#> 2 50 +#> 3 100 +``` +::: + + + +In general, this is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in section TODO when we can run the computations in parallel. + +## Sidebar: Types of Grids + +Since the type of grid mattered for this example, let’s go on a quick “side quest” to talk about the two major types of grids. + +the effect of integers (max size etc) and also on duplicate computations + +## Sidebar: Parallel Processing + + + +## What are Submodels? + + +## How Can we Exploit Submodels? + + +## Types of Postprocessors + +Those needing tuning and those that are just applied. + + + +## Postprocessing Example: PRobability Threshold Optimization + + diff --git a/learn/work/efficient-grid-search/index.qmd b/learn/work/efficient-grid-search/index.qmd new file mode 100644 index 00000000..646b669a --- /dev/null +++ b/learn/work/efficient-grid-search/index.qmd @@ -0,0 +1,200 @@ +--- +title: "Efficient Grid Search" +categories: + - tuning +type: learn-subsection +weight: 4 +description: | + A general discussion on grids and how to efficiently estimate performance. . +toc: true +toc-depth: 2 +include-after-body: ../../../resources.html +--- + + +```{r} +#| label: setup +#| include: false +library(tidymodels) +tidymodels_prefer() +theme_set(theme_bw()) +options(pillar.advice = FALSE, pillar.min_title_chars = Inf) +pkgs <- c("tidymodels") +source(here::here("common.R")) +``` + +## Introduction + +`r article_req_pkgs(pkgs)` + +This article demonstrates .... + + +## The Overall Grid Tuning Procedure + +tidymodels has three phases of creating a model pipeline: + + - preprocessors: computations that prepare data for the model + - supervised model: the actual model fit + - postprocessing: adjustments to model predictions + +Each of these stages can have tuning parameters. Some examples: + +- We might add a spline basis expansion to a predictor to enable to have a nonlinear trend in the model. We take one predictor and create multiple columns that are based on the original column. As we add new spline columns, the model can be more complex. We don't know how many columns to add so we try different values and see which maximizes performance. + +- If many predictors are correlated with one another, we could determine which predictors (and how many) can be removed to reduce multicollinearity. One approach is the have a threshold for the maximum allowable pairwise correlations and have an algorithm remove the smallest set of predictors to satisfy the constraint. The threshold would need to be tuned. + +- Most models have tuning parameters. Boosted trees have many but two of the main parameters are the learning rate and the number of trees in the ensemble. Both usually require tuning. + +- For binary classification models, especially those with a class imbalance, it is possible that the default 50% probability threshold that is used to define what is "an event" does not satisfy the user's needs. For example, when screening blood bank samples, we want a model to err on the side of throwing out disease-free donations as long as we minimize the number of bad blood samples that truly are diseased. In other words, we might want to tune the probability threshold to achieve a high specificity metric. + +Once we know what the tuning parameters are and which phases of the pipeline they are from, we can create a grid of values to evaluate. Since a pipeline can have multiple tuning parameters, a _candidate_ is defined to be a single set of actual parameter values that could be tested. A set of multiple tuning parameter candidate values is a grid. + +For grid search, we evaluate each candidate in the grid by training the entire pipeline, predicting a set of data, and computing performance metrics that estimate the candidate’s efficacy. Once this is finished, we can pick the candidate set that shows the best value. + +This page outlines the general process and describes different constraints and approaches to efficiently evaluating a grid of candidate values. Unexpectedly, the tools that we use to increase efficiency often result in more and more complex routines to compute performance for the grid. + +We’ll break down some of the complications and exploitations that are relevant for model tuning (via grid search). We’ll also start with very simple use-cases and steadily increase the complexity of the task as well as the complexity of the solutions. + +## How Does Training Occur in the Pipeline? + +We should describe how different models would accomplish the three stages of a model pipeline. + +On one hand, there are deep neural networks. These are highly nonlinear models whose model structure is defined as a sequence of layers. The architecture of the network is defined by different types of layers that are intended to do different things. The mantra of deep learning is to add layers to the network that can accomplish all of the tasks discussed above. There is little to no delineation regarding what is a pre- or post-processor; it is all part of the model and all parameters are estimated simultaneously. We will call this type of model a “simultaneous estimation” since it all happens at the same time. + +On the other hand is nearly every other type of model. In most cases, preprocessing tasks are separate estimation procedures that are executed independently of the model fit. There are cases such as principal component regression that conducts PCA signal extraction before passing the results to a routine that executes ordinary least squares. However, there are still two separate estimation tasks that are carried out in sequence, not simultaneously. We’ll call this type of training “stagewise estimation” since there are multiple, distinct training steps that occur in sequence. + +Our focus is 100% on stagewise estimation. + +## Stage-Wise Modeling and Conditional Execution + +One important part of processing a grid of candidate values is to avoid repeating any computations wherever possible. This leads to the idea of conditional execution. Let’s think of an example. + +Suppose our pipeline consists of one preprocess and the model fit (i.e., no postprocessing). Let’s choose a fairly expensive preprocessing technique: UMAP feature extraction. UMAP has a variety of tuning parameters and one is the number of nearest neighbors to use when creating a network of nearby training set samples. Suppose that we will consider values ranging from 1 to 30. + +For our model, we’ll choose a random forest model. This also has tuning parameters and we’ll choose to optimize “m-try”; the number of predictors to randomly select each time a split is created in any decision tree. + +For illustration, let’s use a grid of points with six candidates: + +```{r} +#| label: umap-rf-grid +library(tidymodels) +umap_rf_param <- parameters(neighbors(c(1, 30)), mtry(c(1, 100))) +umap_rf_grid <- grid_regular(umap_rf_param, levels = c(2, 3)) +umap_rf_grid %>% arrange(neighbors, mtry) +``` + +To evaluate these candidates, we could just loop through each row, train a pipeline with that row’s candidate values, predictor a holdout set, and then compute a performance statistic. What does “train a pipeline” mean though? For the first candidate, it means + +- Carry out the UMAP estimation process on the training set using a single nearest neighbor. +- Apply UMAP to the training set and save the transformed data. +- Estimate a random forest model with `mtry = 1` using the transformed training set. + +The first and third steps are two separate estimations. + +The remaining tasks are to + +- Apply UMAP to the holdout data. +- Predict the holdout data with the fitted random forest model. +- Compute the performance statistic of interest (e.g., R2, accuracy, RMSE, etc.) + +For the second candidate, the process is exactly the same _except_ that the random forest model uses `mtry = 50` instead of `mtry = 1`. + +We _have_ to compute a new random forest model since it is a different model. However, the UMAP step is identical to the previous candidate’s preprocessor since it has the same values. Since UMAP is expensive, we end up spending a lot of time doing something that we have already done. + +This would not be the case if there were no connection between the preprocessing tuning parameters and the model parameters, for example, this grid would not have repeated computations: + +```{r} +#| label: umap-rf-grid-other +#| echo: false +umap_rf_grid_other <- umap_rf_grid +umap_rf_grid_other$neighbors <- (1:6) * 5 +umap_rf_grid_other +``` + +In this case, the UMAP step would be different for each candidate and we would have to recompute it anyways. + +Once solution to avoiding redundant computations would be to cache the UMAP computations so that they could be reused later. + +A better solutions is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. For the first grid of candidates contained in `umap_rf_grid`: + +:::: {.columns} + +::: {.column width="10%"} + +::: + +::: {.column width="80%"} + + +```pseudocode +#| html-line-number: true +#| html-line-number-punc: ":" + +\begin{algorithm} +\begin{algorithmic} +\State $\mathfrak{D}^{tr}$: training set +\State $\mathfrak{D}^{ho}$: holdout set +\For{$k \in \{1, 30\}$} + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{tr}$ + \State Apply UMAP to $\mathfrak{D}^{tr}$, creating $\widehat{\mathfrak{D}}^{tr}_k$ + \State Apply UMAP to $\mathfrak{D}^{ho}$, creating $\widehat{\mathfrak{D}}^{ho}_k$ + \For{$m \in \{1, 50, 100\}$} + \State Train a random forest model with $m_{try} = m$ on $\mathfrak{D}^{tr}_k$ to produce $\widehat{f}_{km}$ + \State Predict $\widehat{\mathfrak{D}}^{ho}_k$ with $\widehat{f}_{km}$ + \State Compute performance statistic $\widehat{Q}_{km}$. + \EndFor +\EndFor +\State Determine the $k$ and $m$ calues corresponding to the best value of $\widehat{Q}_{km}$. +\end{algorithmic} +\end{algorithm} +``` + +::: + +::: {.column width="10%"} + +::: + +:::: + + +In this way, we evaluated six candidates via two UMAP models and six random forest models. We avoid four redundant and expensive UMAP fits. + +We can organize this data using a nested structure: + +```{r} +umap_rf_nested_grid <- + umap_rf_grid %>% + group_nest(neighbors, .key = "second_stage") +umap_rf_nested_grid +umap_rf_nested_grid$second_stage[[1]] +``` + +In general, this is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in section TODO when we can run the computations in parallel. + +## Sidebar: Types of Grids + +Since the type of grid mattered for this example, let’s go on a quick “side quest” to talk about the two major types of grids. + +the effect of integers (max size etc) and also on duplicate computations + +## Sidebar: Parallel Processing + + + +## What are Submodels? + + +## How Can we Exploit Submodels? + + +## Types of Postprocessors + +Those needing tuning and those that are just applied. + + + +## Postprocessing Example: PRobability Threshold Optimization + + From 5635551a926728713e6dcbad0eb61467969c380b Mon Sep 17 00:00:00 2001 From: topepo Date: Wed, 27 Nov 2024 12:17:01 -0500 Subject: [PATCH 03/13] add a plot of grids --- learn/work/efficient-grid-search/index.qmd | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/learn/work/efficient-grid-search/index.qmd b/learn/work/efficient-grid-search/index.qmd index 646b669a..0272f788 100644 --- a/learn/work/efficient-grid-search/index.qmd +++ b/learn/work/efficient-grid-search/index.qmd @@ -179,6 +179,34 @@ Since the type of grid mattered for this example, let’s go on a quick “side the effect of integers (max size etc) and also on duplicate computations +```{r} +#| label: fig-two-grids +#| echo: false +#| fig-width: 7 +#| fig-height: 3.5 +#| fig-align: center +#| out-width: 95% + +two_param <- parameters(penalty(), neighbors(c(1, 50))) +two_param_reg <- + two_param %>% + grid_regular(levels = c(5, 6)) %>% + mutate(type = "Regular") + +two_param_sfd <- + two_param %>% + grid_space_filling(size = 5 * 6) %>% + mutate(type = "Space-Filling") + +bind_rows(two_param_reg, two_param_sfd) %>% + ggplot(aes(penalty, neighbors)) + + geom_point() + + scale_x_log10() + + facet_wrap(~ type) + + coord_fixed(ratio = 1 / 5) + +``` + ## Sidebar: Parallel Processing From 9cd97f51101d3d01379aeaa50f5e83efabc8725f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98topepo=E2=80=99?= Date: Wed, 27 Nov 2024 12:51:36 -0500 Subject: [PATCH 04/13] add a postprocesing algorithm --- learn/work/efficient-grid-search/index.qmd | 181 +++++++++++++++++++-- 1 file changed, 171 insertions(+), 10 deletions(-) diff --git a/learn/work/efficient-grid-search/index.qmd b/learn/work/efficient-grid-search/index.qmd index 0272f788..7b965e7a 100644 --- a/learn/work/efficient-grid-search/index.qmd +++ b/learn/work/efficient-grid-search/index.qmd @@ -88,7 +88,7 @@ To evaluate these candidates, we could just loop through each row, train a pipel - Carry out the UMAP estimation process on the training set using a single nearest neighbor. - Apply UMAP to the training set and save the transformed data. -- Estimate a random forest model with `mtry = 1` using the transformed training set. +- Estimate a random forest model with $m_{try} = 1$ using the transformed training set. The first and third steps are two separate estimations. @@ -96,9 +96,9 @@ The remaining tasks are to - Apply UMAP to the holdout data. - Predict the holdout data with the fitted random forest model. -- Compute the performance statistic of interest (e.g., R2, accuracy, RMSE, etc.) +- Compute the performance statistic of interest (e.g., $R^2$, accuracy, RMSE, etc.) -For the second candidate, the process is exactly the same _except_ that the random forest model uses `mtry = 50` instead of `mtry = 1`. +For the second candidate, the process is exactly the same _except_ that the random forest model uses $m_{try} = 50$ instead of $m_{try} = 1$. We _have_ to compute a new random forest model since it is a different model. However, the UMAP step is identical to the previous candidate’s preprocessor since it has the same values. Since UMAP is expensive, we end up spending a lot of time doing something that we have already done. @@ -141,11 +141,11 @@ A better solutions is _conditional execution_. In this case, we determine the un \State Apply UMAP to $\mathfrak{D}^{ho}$, creating $\widehat{\mathfrak{D}}^{ho}_k$ \For{$m \in \{1, 50, 100\}$} \State Train a random forest model with $m_{try} = m$ on $\mathfrak{D}^{tr}_k$ to produce $\widehat{f}_{km}$ - \State Predict $\widehat{\mathfrak{D}}^{ho}_k$ with $\widehat{f}_{km}$ - \State Compute performance statistic $\widehat{Q}_{km}$. + \State Predict $\widehat{\mathfrak{D}}^{ho}_k$ with $\widehat{f}_{k,m}$ + \State Compute performance statistic $\widehat{Q}_{k,m}$. \EndFor \EndFor -\State Determine the $k$ and $m$ calues corresponding to the best value of $\widehat{Q}_{km}$. +\State Determine the $k$ and $m$ values corresponding to the best value of $\widehat{Q}_{k,m}$. \end{algorithmic} \end{algorithm} ``` @@ -164,11 +164,11 @@ In this way, we evaluated six candidates via two UMAP models and six random fore We can organize this data using a nested structure: ```{r} -umap_rf_nested_grid <- +umap_rf_schedule <- umap_rf_grid %>% group_nest(neighbors, .key = "second_stage") -umap_rf_nested_grid -umap_rf_nested_grid$second_stage[[1]] +umap_rf_schedule +umap_rf_schedule$second_stage[[1]] ``` In general, this is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in section TODO when we can run the computations in parallel. @@ -179,6 +179,7 @@ Since the type of grid mattered for this example, let’s go on a quick “side the effect of integers (max size etc) and also on duplicate computations + ```{r} #| label: fig-two-grids #| echo: false @@ -207,22 +208,182 @@ bind_rows(two_param_reg, two_param_sfd) %>% ``` + ## Sidebar: Parallel Processing ## What are Submodels? +“Submodels” is a situation where a model can make predictions for different tuning parameter values _without_ retraining. Two examples: + +- _Boosting_ ensembles take the same model and, using different case weights, create a sequential set of fits. For example, a tree ensemble created with 100 boosting iterations will contain 100 trees, each depending on the previous fits. For many implementations, you can fit the largest ensemble size (100 in this example) and, for free, make predictions on sizes 1 through 99. +- Some regularized models, such as glmnet, have a penalization parameter that attenuates the magnitude of the regression coefficients (think weight decay in neural networks). We often tune over this parameter. For glmnet, the model simultaneously creates a _path_ of possible penalty values. With a single model fit, you can predict on _any_ penalty values. + +Not all models contain parameters that can use the “submodel trick.” However, when they do, there can be a massive efficient gain with grid search. + + ## How Can we Exploit Submodels? +Let's use boosting as an example. We'll repeat our UMAP preprocessor and tune over: + + - The number of boosted trees in an ensemble with values 1 through 100. + - The minimum number of data points required to keep splitting the tree (aka `min_n`) for values of 1, 20, and 40. + +With the same UMAP grid, let’s use is $2 \times 100 \times 3 = 600$ grid points. + +```{r} +#| label: full-boosting-grid +umap_boost_param <- parameters(neighbors(c(1, 30)), min_n(c(1, 40)), trees(c(1, 100))) +umap_boost_grid <- grid_regular(umap_boost_param, levels = c(2, 3, 100)) +umap_boost_grid +``` + +There are six unique combinations of the number of neighbors and `min_n`, so we can evaluate only six models using these parameters: + +```{r} +#| label: min-boosting-grid +models_to_fit <- + umap_boost_grid %>% + summarize(trees = max(trees), .by = c(neighbors, min_n)) +models_to_fit +``` + +```{r} +#| label: min-boosting-submodel-grid +submodels <- + umap_boost_grid %>% + group_nest(neighbors, min_n, .key = "predict_with") + +submodel_schedule <- + models_to_fit %>% + full_join(submodels, by = c("neighbors", "min_n")) + +submodel_schedule +submodel_schedule$predict_with[[1]] +``` + + + +With submodels, for efficiency, there are three loops to most efficiently evaluate this grid. + + +:::: {.columns} + +::: {.column width="10%"} + +::: + +::: {.column width="80%"} + + +```pseudocode +#| html-line-number: true +#| html-line-number-punc: ":" + +\begin{algorithm} +\begin{algorithmic} +\State $\mathfrak{D}^{tr}$: training set +\State $\mathfrak{D}^{ho}$: holdout set +\For{$k \in \{1, 30\}$} + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{tr}$ + \State Apply UMAP to $\mathfrak{D}^{tr}$, creating $\widehat{\mathfrak{D}}^{tr}_k$ + \State Apply UMAP to $\mathfrak{D}^{ho}$, creating $\widehat{\mathfrak{D}}^{ho}_k$ + \For{$n \in \{1, 20, 40\}$} + \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{tr}_k$ to produce $\widehat{f}_{k,n,100}$ + \For{$t \in \{1, 2, \ldots, 100\}$} + \State Predict $\widehat{\mathfrak{D}}^{ho}_k$ with $\widehat{f}_{k,n,100}$ stopping at $t$ trees + \State Compute performance statistic $\widehat{Q}_{k,n,t}$. + \EndFor + \EndFor +\EndFor +\State Determine the $k$, $n$, and $t$ values corresponding to the best value of $\widehat{Q}_{k,n,t}$. +\end{algorithmic} +\end{algorithm} +``` + +::: + +::: {.column width="10%"} + +::: + +:::: + + + + ## Types of Postprocessors Those needing tuning and those that are just applied. +## Postprocessing Example: Probability Threshold Optimization -## Postprocessing Example: PRobability Threshold Optimization +```{r} +#| label: full-umap-boosting-threshold-grid +umap_boost_thrsh_grid <- crossing(umap_boost_grid, threshold = (5:10)/10) + +post_submodels <- + umap_boost_thrsh_grid %>% + group_nest(neighbors, min_n, .key = "predict_with") %>% + mutate( + predict_with = map(predict_with, ~ .x %>% group_nest(trees, .key = "post")) + ) + +post_schedule <- + models_to_fit %>% + full_join(post_submodels, by = c("neighbors", "min_n")) + +post_schedule +post_schedule$predict_with[[1]] +post_schedule$predict_with[[1]]$post[[1]] +``` + +:::: {.columns} + +::: {.column width="10%"} +::: + +::: {.column width="80%"} + + +```pseudocode +#| html-line-number: true +#| html-line-number-punc: ":" + +\begin{algorithm} +\begin{algorithmic} +\State $\mathfrak{D}^{tr}$: training set +\State $\mathfrak{D}^{ho}$: holdout set +\For{$k \in \{1, 30\}$} + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{tr}$ + \State Apply UMAP to $\mathfrak{D}^{tr}$, creating $\widehat{\mathfrak{D}}^{tr}_k$ + \State Apply UMAP to $\mathfrak{D}^{ho}$, creating $\widehat{\mathfrak{D}}^{ho}_k$ + \For{$n \in \{1, 20, 40\}$} + \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{tr}_k$ to produce $\widehat{f}_{k,n,100}$ + \For{$t \in \{1, 2, \ldots, 100\}$} + \State Predict $\widehat{\mathfrak{D}}^{ho}_k$ with $\widehat{f}_{k,n,100}$ stopping at $t$ trees + \For{$\pi \in \{0.5, 0.6, \ldots, 1.0\}$} + \State Recompute hard class predictions with threshold $\pi$. + \State Compute performance statistic $\widehat{Q}_{k,n,t,\pi}$. + \EndFor + \EndFor + \EndFor +\EndFor +\State Determine the $k$, $n$, $t$, and $\pi$ values corresponding to the best value of $\widehat{Q}_{k,n,t,\pi}$. +\end{algorithmic} +\end{algorithm} +``` + +::: + +::: {.column width="10%"} + +::: + +:::: From 6af32069a7c33fc18021fb2aa291e66572bf5325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98topepo=E2=80=99?= Date: Wed, 27 Nov 2024 14:37:33 -0500 Subject: [PATCH 05/13] added resampling and parallel parking --- learn/work/efficient-grid-search/index.qmd | 122 ++++++++++++++++----- 1 file changed, 94 insertions(+), 28 deletions(-) diff --git a/learn/work/efficient-grid-search/index.qmd b/learn/work/efficient-grid-search/index.qmd index 7b965e7a..40ee4375 100644 --- a/learn/work/efficient-grid-search/index.qmd +++ b/learn/work/efficient-grid-search/index.qmd @@ -29,6 +29,8 @@ source(here::here("common.R")) This article demonstrates .... +Consider it an expanded version of Section 13.5 of [_Tidy Models with R_](https://www.tmwr.org/grid-search#efficient-grids). + ## The Overall Grid Tuning Procedure @@ -112,11 +114,11 @@ umap_rf_grid_other$neighbors <- (1:6) * 5 umap_rf_grid_other ``` -In this case, the UMAP step would be different for each candidate and we would have to recompute it anyways. +In this case, the UMAP step would be different for each candidate so there is no computational redundancy to eliminate. Once solution to avoiding redundant computations would be to cache the UMAP computations so that they could be reused later. -A better solutions is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. For the first grid of candidates contained in `umap_rf_grid`: +A better solution is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. For the first grid of candidates contained in `umap_rf_grid`: :::: {.columns} @@ -133,15 +135,15 @@ A better solutions is _conditional execution_. In this case, we determine the un \begin{algorithm} \begin{algorithmic} -\State $\mathfrak{D}^{tr}$: training set -\State $\mathfrak{D}^{ho}$: holdout set +\State $\mathfrak{D}^{fit}$: training set +\State $\mathfrak{D}^{pred}$: holdout set \For{$k \in \{1, 30\}$} - \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{tr}$ - \State Apply UMAP to $\mathfrak{D}^{tr}$, creating $\widehat{\mathfrak{D}}^{tr}_k$ - \State Apply UMAP to $\mathfrak{D}^{ho}$, creating $\widehat{\mathfrak{D}}^{ho}_k$ + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}$ + \State Apply UMAP to $\mathfrak{D}^{fit}$, creating $\widehat{\mathfrak{D}}^{fit}_k$ + \State Apply UMAP to $\mathfrak{D}^{pred}$, creating $\widehat{\mathfrak{D}}^{pred}_k$ \For{$m \in \{1, 50, 100\}$} - \State Train a random forest model with $m_{try} = m$ on $\mathfrak{D}^{tr}_k$ to produce $\widehat{f}_{km}$ - \State Predict $\widehat{\mathfrak{D}}^{ho}_k$ with $\widehat{f}_{k,m}$ + \State Train a random forest model with $m_{try} = m$ on $\mathfrak{D}^{fit}_k$ to produce $\widehat{f}_{km}$ + \State Predict $\widehat{\mathfrak{D}}^{pred}_k$ with $\widehat{f}_{k,m}$ \State Compute performance statistic $\widehat{Q}_{k,m}$. \EndFor \EndFor @@ -171,14 +173,15 @@ umap_rf_schedule umap_rf_schedule$second_stage[[1]] ``` -In general, this is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in section TODO when we can run the computations in parallel. +In general, conditional execution is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in section TODO when we can run the computations in parallel. ## Sidebar: Types of Grids Since the type of grid mattered for this example, let’s go on a quick “side quest” to talk about the two major types of grids. -the effect of integers (max size etc) and also on duplicate computations +When we think about parameter grids, there are two types: regular and irregular. A regular grid is one where we create a univariate sequence of values for each tuning parameter and then create all possible combinations. Irregular grids can be made in many different ways but cannot be combinatorial in nature. Examples are random grids (i.e., simulating random values across parameter ranges), latin hypercube designs, and others. The best irregular grids are _space-filling designs_; they attempt to cover the entire parameter space and try to distribute the points so that none are overly similar to the others. +Here’s an example of a regular grid and a space-filling design where each has 15 candidate points: ```{r} #| label: fig-two-grids @@ -191,12 +194,12 @@ the effect of integers (max size etc) and also on duplicate computations two_param <- parameters(penalty(), neighbors(c(1, 50))) two_param_reg <- two_param %>% - grid_regular(levels = c(5, 6)) %>% + grid_regular(levels = c(5, 3)) %>% mutate(type = "Regular") two_param_sfd <- two_param %>% - grid_space_filling(size = 5 * 6) %>% + grid_space_filling(size = 3 * 5) %>% mutate(type = "Space-Filling") bind_rows(two_param_reg, two_param_sfd) %>% @@ -205,12 +208,75 @@ bind_rows(two_param_reg, two_param_sfd) %>% scale_x_log10() + facet_wrap(~ type) + coord_fixed(ratio = 1 / 5) - ``` +Space-filling designs are, on average, the best approach for parameter tuning. They need far fewer grid points to cover the parameter space than regular grids. + +The choice of grid types is relevant here because it affects the tactics used to efficiently process the grid. Consider the two grids above. Our conditional execution approach works well for the regular grid; for each value of the number of neighbors, there is a nice vector of values for the other parameter. Conversely, for the space-filling design, there is only a single corresponding penalty value for each unique number of neighbors. No conditional execution is possible. + +## There's Something We Didn't Tell You + +One important aspect of grid search that we have obfuscated in the algorithm is shown in the previous section. Line 10 says “Compute performance statistic $\widehat{Q}_{k,n,t}$” which left a lot out. + +In that same algorithm, we had two data sets: a training set ($\mathfrak{D}^{fit}$) and a holdout set ($\mathfrak{D}^{pred}$) of data points that were not included in the training set. We said “holdout” instead of “test set” for generality. + +When tuning models, a single holdout set (a.k.a. a validation set) or multiple holdout sets are used (via resampling). For example, with 10-fold cross-validation, there is _another_ loop over the number resamples. + + +:::: {.columns} + +::: {.column width="10%"} + +::: + +::: {.column width="80%"} + + +```pseudocode +#| html-line-number: true +#| html-line-number-punc: ":" + +\begin{algorithm} +\begin{algorithmic} +\For{$b = 1, \ldots, B$} + \State Split the data into $\mathfrak{D}^{fit}_b$ and $\mathfrak{D}^{pred}_b$ + \For{$k \in \{1, 30\}$} + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}_b$ + \State Apply UMAP to $\mathfrak{D}^{fit}_b$, creating $\widehat{\mathfrak{D}}^{fit}_{b,k}$ + \State Apply UMAP to $\mathfrak{D}^{pred}_b$, creating $\widehat{\mathfrak{D}}^{pred}_{b,k}$ + \For{$m \in \{1, 50, 100\}$} + \State Train a random forest model with $m_{try} = m$ on $\mathfrak{D}^{fit}_{b,k}$ to produce $\widehat{f}_{b,k,m}$ + \State Predict $\widehat{\mathfrak{D}}^{pred}_k$ with $\widehat{f}_{b,k,m}$ + \State Compute performance statistic $\widehat{Q}_{b,k,m}$. + \EndFor + \EndFor +\EndFor +\State Compute the average performance metric $\widehat{Q}_{k,m} = 1/B\sum \widehat{Q}_{b,k,m}$ +\State Determine the $k$ and $m$ values corresponding to the best value of $\widehat{Q}_{k,m}$. +\end{algorithmic} +\end{algorithm} +``` + +::: + +::: {.column width="10%"} + +::: + +:::: + + ## Sidebar: Parallel Processing +Another tool to increase computational efficiency is parallel processing. We have multiple computational cores on our computers that can perform calculations simultaneously. If we have $B$ resamples and $s$ candidates in our grid, there are $B \times s$ model fits to evaluate and none of them depend on the others^[That’s true but only kind of true; we’ll elaborate in the section on submodels below.]. We can split up these operations into chunks and send these chunks to different computer cores. + +This is discussed at leangth in Section 13.5.2 of [_Tidy Models with R_](https://www.tmwr.org/grid-search#parallel-processing). Here are the highlights: + +- You can almost always reduce the time to process a grid using parallelism. +- How the tasks are chunked can matter a lot. For example: + - If you have a lot of data and I/O is expensive, you might want to chunk the data so that all the computations use the same data. For example, for resampling, you might send all tasks for $\mathfrak{D}^{fit}_b$ and $\mathfrak{D}^{pred}_b$ are on the same computer core. +- If your preprocessing (if any) is very fast, you may want to flatten the loops so that the computational tasks loop over each of the $B \times s$ resample/candidate combinations. This means that the preprocessing will be recalculated several times but, if they are cheap, this might be the fasted approach. ## What are Submodels? @@ -284,16 +350,16 @@ With submodels, for efficiency, there are three loops to most efficiently evalua \begin{algorithm} \begin{algorithmic} -\State $\mathfrak{D}^{tr}$: training set -\State $\mathfrak{D}^{ho}$: holdout set +\State $\mathfrak{D}^{fit}$: training set +\State $\mathfrak{D}^{pred}$: holdout set \For{$k \in \{1, 30\}$} - \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{tr}$ - \State Apply UMAP to $\mathfrak{D}^{tr}$, creating $\widehat{\mathfrak{D}}^{tr}_k$ - \State Apply UMAP to $\mathfrak{D}^{ho}$, creating $\widehat{\mathfrak{D}}^{ho}_k$ + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}$ + \State Apply UMAP to $\mathfrak{D}^{fit}$, creating $\widehat{\mathfrak{D}}^{fit}_k$ + \State Apply UMAP to $\mathfrak{D}^{pred}$, creating $\widehat{\mathfrak{D}}^{pred}_k$ \For{$n \in \{1, 20, 40\}$} - \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{tr}_k$ to produce $\widehat{f}_{k,n,100}$ + \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{fit}_k$ to produce $\widehat{f}_{k,n,100}$ \For{$t \in \{1, 2, \ldots, 100\}$} - \State Predict $\widehat{\mathfrak{D}}^{ho}_k$ with $\widehat{f}_{k,n,100}$ stopping at $t$ trees + \State Predict $\widehat{\mathfrak{D}}^{pred}_k$ with $\widehat{f}_{k,n,100}$ stopping at $t$ trees \State Compute performance statistic $\widehat{Q}_{k,n,t}$. \EndFor \EndFor @@ -357,16 +423,16 @@ post_schedule$predict_with[[1]]$post[[1]] \begin{algorithm} \begin{algorithmic} -\State $\mathfrak{D}^{tr}$: training set -\State $\mathfrak{D}^{ho}$: holdout set +\State $\mathfrak{D}^{fit}$: training set +\State $\mathfrak{D}^{pred}$: holdout set \For{$k \in \{1, 30\}$} - \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{tr}$ - \State Apply UMAP to $\mathfrak{D}^{tr}$, creating $\widehat{\mathfrak{D}}^{tr}_k$ - \State Apply UMAP to $\mathfrak{D}^{ho}$, creating $\widehat{\mathfrak{D}}^{ho}_k$ + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}$ + \State Apply UMAP to $\mathfrak{D}^{fit}$, creating $\widehat{\mathfrak{D}}^{fit}_k$ + \State Apply UMAP to $\mathfrak{D}^{pred}$, creating $\widehat{\mathfrak{D}}^{pred}_k$ \For{$n \in \{1, 20, 40\}$} - \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{tr}_k$ to produce $\widehat{f}_{k,n,100}$ + \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{fit}_k$ to produce $\widehat{f}_{k,n,100}$ \For{$t \in \{1, 2, \ldots, 100\}$} - \State Predict $\widehat{\mathfrak{D}}^{ho}_k$ with $\widehat{f}_{k,n,100}$ stopping at $t$ trees + \State Predict $\widehat{\mathfrak{D}}^{pred}_k$ with $\widehat{f}_{k,n,100}$ stopping at $t$ trees \For{$\pi \in \{0.5, 0.6, \ldots, 1.0\}$} \State Recompute hard class predictions with threshold $\pi$. \State Compute performance statistic $\widehat{Q}_{k,n,t,\pi}$. From bd33e262aac2d35587c50dfbdd96a803b5fca36a Mon Sep 17 00:00:00 2001 From: topepo Date: Thu, 28 Nov 2024 12:25:17 -0500 Subject: [PATCH 06/13] redo the algorithm and code for submodel execution --- learn/work/efficient-grid-search/index.qmd | 134 ++++++++++++--------- 1 file changed, 79 insertions(+), 55 deletions(-) diff --git a/learn/work/efficient-grid-search/index.qmd b/learn/work/efficient-grid-search/index.qmd index 40ee4375..27b15faf 100644 --- a/learn/work/efficient-grid-search/index.qmd +++ b/learn/work/efficient-grid-search/index.qmd @@ -68,7 +68,7 @@ On the other hand is nearly every other type of model. In most cases, preprocess Our focus is 100% on stagewise estimation. -## Stage-Wise Modeling and Conditional Execution +## Stage-Wise Modeling and Conditional Execution {#sec-stagewise-exe} One important part of processing a grid of candidate values is to avoid repeating any computations wherever possible. This leads to the idea of conditional execution. Let’s think of an example. @@ -118,7 +118,7 @@ In this case, the UMAP step would be different for each candidate so there is no Once solution to avoiding redundant computations would be to cache the UMAP computations so that they could be reused later. -A better solution is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. For the first grid of candidates contained in `umap_rf_grid`: +A better solution is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. Here's a listing of this algorithm: :::: {.columns} @@ -169,13 +169,17 @@ We can organize this data using a nested structure: umap_rf_schedule <- umap_rf_grid %>% group_nest(neighbors, .key = "second_stage") + +# The training loop to iterate over: umap_rf_schedule + +# Within that loop, iterative over these: umap_rf_schedule$second_stage[[1]] ``` In general, conditional execution is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in section TODO when we can run the computations in parallel. -## Sidebar: Types of Grids +## Sidebar: Types of Grids {#sec-grid-types} Since the type of grid mattered for this example, let’s go on a quick “side quest” to talk about the two major types of grids. @@ -214,7 +218,7 @@ Space-filling designs are, on average, the best approach for parameter tuning. T The choice of grid types is relevant here because it affects the tactics used to efficiently process the grid. Consider the two grids above. Our conditional execution approach works well for the regular grid; for each value of the number of neighbors, there is a nice vector of values for the other parameter. Conversely, for the space-filling design, there is only a single corresponding penalty value for each unique number of neighbors. No conditional execution is possible. -## There's Something We Didn't Tell You +## There's Something We Didn't Tell You {#sec-add-in-resampling} One important aspect of grid search that we have obfuscated in the algorithm is shown in the previous section. Line 10 says “Compute performance statistic $\widehat{Q}_{k,n,t}$” which left a lot out. @@ -246,7 +250,7 @@ When tuning models, a single holdout set (a.k.a. a validation set) or multiple h \State Apply UMAP to $\mathfrak{D}^{pred}_b$, creating $\widehat{\mathfrak{D}}^{pred}_{b,k}$ \For{$m \in \{1, 50, 100\}$} \State Train a random forest model with $m_{try} = m$ on $\mathfrak{D}^{fit}_{b,k}$ to produce $\widehat{f}_{b,k,m}$ - \State Predict $\widehat{\mathfrak{D}}^{pred}_k$ with $\widehat{f}_{b,k,m}$ + \State Predict $\widehat{\mathfrak{D}}^{pred}_{b,k}$ with $\widehat{f}_{b,k,m}$ \State Compute performance statistic $\widehat{Q}_{b,k,m}$. \EndFor \EndFor @@ -267,9 +271,9 @@ When tuning models, a single holdout set (a.k.a. a validation set) or multiple h -## Sidebar: Parallel Processing +## Sidebar: Parallel Processing {#sec-in-parallel} -Another tool to increase computational efficiency is parallel processing. We have multiple computational cores on our computers that can perform calculations simultaneously. If we have $B$ resamples and $s$ candidates in our grid, there are $B \times s$ model fits to evaluate and none of them depend on the others^[That’s true but only kind of true; we’ll elaborate in the section on submodels below.]. We can split up these operations into chunks and send these chunks to different computer cores. +Another tool to increase computational efficiency is parallel processing. We have multiple computational cores on our computers that can perform calculations simultaneously. If we have $B$ resamples and $s$ candidates in our grid, there are $B \times s$ model fits to evaluate and none of them depend on the others^[That’s true but only _kind of true_; we’ll elaborate in @sec-submodels below.]. We can split up these operations into chunks and send these chunks to different computer cores. This is discussed at leangth in Section 13.5.2 of [_Tidy Models with R_](https://www.tmwr.org/grid-search#parallel-processing). Here are the highlights: @@ -279,18 +283,18 @@ This is discussed at leangth in Section 13.5.2 of [_Tidy Models with R_](https:/ - If your preprocessing (if any) is very fast, you may want to flatten the loops so that the computational tasks loop over each of the $B \times s$ resample/candidate combinations. This means that the preprocessing will be recalculated several times but, if they are cheap, this might be the fasted approach. -## What are Submodels? +## What are Submodels? {#sec-submodels} “Submodels” is a situation where a model can make predictions for different tuning parameter values _without_ retraining. Two examples: - _Boosting_ ensembles take the same model and, using different case weights, create a sequential set of fits. For example, a tree ensemble created with 100 boosting iterations will contain 100 trees, each depending on the previous fits. For many implementations, you can fit the largest ensemble size (100 in this example) and, for free, make predictions on sizes 1 through 99. - Some regularized models, such as glmnet, have a penalization parameter that attenuates the magnitude of the regression coefficients (think weight decay in neural networks). We often tune over this parameter. For glmnet, the model simultaneously creates a _path_ of possible penalty values. With a single model fit, you can predict on _any_ penalty values. -Not all models contain parameters that can use the “submodel trick.” However, when they do, there can be a massive efficient gain with grid search. +Not all models contain parameters that can use the “submodel trick.” For example, the random forest model from @sec-stagewise-exe is also an ensemble of trees. However, those trees are not created in a sequence; they are all independent of one another. +However, when our model has submodel parameters, there can be a massive efficient gain with grid search. - -## How Can we Exploit Submodels? +## How Can we Exploit Submodels? {#sec-submodel-processing} Let's use boosting as an example. We'll repeat our UMAP preprocessor and tune over: @@ -316,24 +320,7 @@ models_to_fit <- models_to_fit ``` -```{r} -#| label: min-boosting-submodel-grid -submodels <- - umap_boost_grid %>% - group_nest(neighbors, min_n, .key = "predict_with") - -submodel_schedule <- - models_to_fit %>% - full_join(submodels, by = c("neighbors", "min_n")) - -submodel_schedule -submodel_schedule$predict_with[[1]] -``` - - - -With submodels, for efficiency, there are three loops to most efficiently evaluate this grid. - +With submodels, our algorithm has yet another nested loop in Lines 9-11: :::: {.columns} @@ -350,20 +337,22 @@ With submodels, for efficiency, there are three loops to most efficiently evalua \begin{algorithm} \begin{algorithmic} -\State $\mathfrak{D}^{fit}$: training set -\State $\mathfrak{D}^{pred}$: holdout set -\For{$k \in \{1, 30\}$} - \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}$ - \State Apply UMAP to $\mathfrak{D}^{fit}$, creating $\widehat{\mathfrak{D}}^{fit}_k$ - \State Apply UMAP to $\mathfrak{D}^{pred}$, creating $\widehat{\mathfrak{D}}^{pred}_k$ - \For{$n \in \{1, 20, 40\}$} - \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{fit}_k$ to produce $\widehat{f}_{k,n,100}$ - \For{$t \in \{1, 2, \ldots, 100\}$} - \State Predict $\widehat{\mathfrak{D}}^{pred}_k$ with $\widehat{f}_{k,n,100}$ stopping at $t$ trees - \State Compute performance statistic $\widehat{Q}_{k,n,t}$. +\For{$b = 1, \ldots, B$} + \State Split the data into $\mathfrak{D}^{fit}_b$ and $\mathfrak{D}^{pred}_b$ + \For{$k \in \{1, 30\}$} + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}_b$ + \State Apply UMAP to $\mathfrak{D}^{fit}_b$, creating $\widehat{\mathfrak{D}}^{fit}_{b,k}$ + \State Apply UMAP to $\mathfrak{D}^{pred}_b$, creating $\widehat{\mathfrak{D}}^{pred}_{b,k}$ + \For{$n \in \{1, 20, 40\}$} + \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{fit}_{b,k}$ to produce $\widehat{f}_{b,k,n,100}$ + \For{$t \in \{1, 2, \ldots, 100\}$} + \State Predict $\widehat{\mathfrak{D}}^{pred}_{b,k}$ with $\widehat{f}_{b,k,n,100}$ using at $t$ trees + \State Compute performance statistic $\widehat{Q}_{b,k,n,t}$. + \EndFor \EndFor \EndFor \EndFor +\State Compute the average performance metric $\widehat{Q}_{k,n,t} = 1/B\sum \widehat{Q}_{b,k,n,t}$ \State Determine the $k$, $n$, and $t$ values corresponding to the best value of $\widehat{Q}_{k,n,t}$. \end{algorithmic} \end{algorithm} @@ -377,16 +366,49 @@ With submodels, for efficiency, there are three loops to most efficiently evalua :::: +This added complexity has huge benefits in efficiency since our loop for the number of trees ($t$) is for _predicting_ and not for _training_. +```{r} +#| label: min-boosting-submodel-grid +submodel_schedule <- + umap_boost_grid %>% + # First collapse the submodel parameters + group_nest(neighbors, min_n, .key = "predict_stage") %>% + # Now find the number of trees to fit + full_join( + umap_boost_grid %>% + summarize(trees = max(trees), .by = c(neighbors, min_n)), + by = c("neighbors", "min_n") + ) %>% + relocate(trees, .after = min_n) %>% + # Now collapse over the preprocessor for conditional execution + group_nest(neighbors, .key = "second_stage") + +submodel_schedule +``` + +Let's look at one iteration of the loop. For the model with a single neighbor, what is the first boosted tree that we fit? + +```{r} +#| label: min-boosting-submodel-grid-2nd +submodel_schedule$second_stage[[1]] +``` + +For the first of these, with `min_n` equal to `r submodel_schedule$second_stage[[1]]$min_n[1]` and 100 trees, what is the set of tree sizes that we should predict? + +```{r} +#| label: min-boosting-submodel-grid-predict +submodel_schedule$second_stage[[1]]$predict_stage[[1]] +``` -## Types of Postprocessors +## Types of Postprocessors {#sec-postprocessors} Those needing tuning and those that are just applied. -## Postprocessing Example: Probability Threshold Optimization +## Postprocessing Example: Probability Threshold Optimization {#sec-prob-thresholds} ```{r} #| label: full-umap-boosting-threshold-grid @@ -423,23 +445,25 @@ post_schedule$predict_with[[1]]$post[[1]] \begin{algorithm} \begin{algorithmic} -\State $\mathfrak{D}^{fit}$: training set -\State $\mathfrak{D}^{pred}$: holdout set -\For{$k \in \{1, 30\}$} - \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}$ - \State Apply UMAP to $\mathfrak{D}^{fit}$, creating $\widehat{\mathfrak{D}}^{fit}_k$ - \State Apply UMAP to $\mathfrak{D}^{pred}$, creating $\widehat{\mathfrak{D}}^{pred}_k$ - \For{$n \in \{1, 20, 40\}$} - \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{fit}_k$ to produce $\widehat{f}_{k,n,100}$ - \For{$t \in \{1, 2, \ldots, 100\}$} - \State Predict $\widehat{\mathfrak{D}}^{pred}_k$ with $\widehat{f}_{k,n,100}$ stopping at $t$ trees - \For{$\pi \in \{0.5, 0.6, \ldots, 1.0\}$} - \State Recompute hard class predictions with threshold $\pi$. - \State Compute performance statistic $\widehat{Q}_{k,n,t,\pi}$. +\For{$b = 1, \ldots, B$} + \State Split the data into $\mathfrak{D}^{fit}_b$ and $\mathfrak{D}^{pred}_b$ + \For{$k \in \{1, 30\}$} + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}_b$ + \State Apply UMAP to $\mathfrak{D}^{fit}_b$, creating $\widehat{\mathfrak{D}}^{fit}_{b,k}$ + \State Apply UMAP to $\mathfrak{D}^{pred}_b$, creating $\widehat{\mathfrak{D}}^{pred}_{b,k}$ + \For{$n \in \{1, 20, 40\}$} + \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{fit}_{b,k}$ to produce $\widehat{f}_{b,k,n,100}$ + \For{$t \in \{1, 2, \ldots, 100\}$} + \State Predict $\widehat{\mathfrak{D}}^{pred}_{b,k}$ with $\widehat{f}_{b,k,n,100}$ using at $t$ trees + \For{$\pi \in \{0.5, 0.6, \ldots, 1.0\}$} + \State Recompute hard class predictions with threshold $\pi$. + \State Compute performance statistic $\widehat{Q}_{b,k,n,t,\pi}$. + \EndFor \EndFor \EndFor \EndFor \EndFor +\State Compute the average performance metric $\widehat{Q}_{k,n,t,\pi} = 1/B\sum \widehat{Q}_{b,k,n,t,\pi}$ \State Determine the $k$, $n$, $t$, and $\pi$ values corresponding to the best value of $\widehat{Q}_{k,n,t,\pi}$. \end{algorithmic} \end{algorithm} From a74f0f7f7a587d796183b0f7c3d02ffb0b8babee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98topepo=E2=80=99?= Date: Fri, 29 Nov 2024 13:41:13 -0500 Subject: [PATCH 07/13] rework nesting code for postprocessors --- learn/work/efficient-grid-search/index.qmd | 73 +++++++++++++++------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/learn/work/efficient-grid-search/index.qmd b/learn/work/efficient-grid-search/index.qmd index 27b15faf..3897d6cd 100644 --- a/learn/work/efficient-grid-search/index.qmd +++ b/learn/work/efficient-grid-search/index.qmd @@ -56,7 +56,7 @@ For grid search, we evaluate each candidate in the grid by training the entire p This page outlines the general process and describes different constraints and approaches to efficiently evaluating a grid of candidate values. Unexpectedly, the tools that we use to increase efficiency often result in more and more complex routines to compute performance for the grid. -We’ll break down some of the complications and exploitations that are relevant for model tuning (via grid search). We’ll also start with very simple use-cases and steadily increase the complexity of the task as well as the complexity of the solutions. +We’ll break down some of the complications and exploitations that are relevant for model tuning (via grid search). We’ll also start with very simple use-cases and steadily increase the complexity of the task as well as the complexity of the solutions. The examples that we’ll work through are the most complicated. In many cases, none of these optimizations are required (or feasible). ## How Does Training Occur in the Pipeline? @@ -66,7 +66,7 @@ On one hand, there are deep neural networks. These are highly nonlinear models w On the other hand is nearly every other type of model. In most cases, preprocessing tasks are separate estimation procedures that are executed independently of the model fit. There are cases such as principal component regression that conducts PCA signal extraction before passing the results to a routine that executes ordinary least squares. However, there are still two separate estimation tasks that are carried out in sequence, not simultaneously. We’ll call this type of training “stagewise estimation” since there are multiple, distinct training steps that occur in sequence. -Our focus is 100% on stagewise estimation. +Our focus is 100% on stagewise estimation. ## Stage-Wise Modeling and Conditional Execution {#sec-stagewise-exe} @@ -410,25 +410,6 @@ Those needing tuning and those that are just applied. ## Postprocessing Example: Probability Threshold Optimization {#sec-prob-thresholds} -```{r} -#| label: full-umap-boosting-threshold-grid -umap_boost_thrsh_grid <- crossing(umap_boost_grid, threshold = (5:10)/10) - -post_submodels <- - umap_boost_thrsh_grid %>% - group_nest(neighbors, min_n, .key = "predict_with") %>% - mutate( - predict_with = map(predict_with, ~ .x %>% group_nest(trees, .key = "post")) - ) - -post_schedule <- - models_to_fit %>% - full_join(post_submodels, by = c("neighbors", "min_n")) - -post_schedule -post_schedule$predict_with[[1]] -post_schedule$predict_with[[1]]$post[[1]] -``` :::: {.columns} @@ -477,3 +458,53 @@ post_schedule$predict_with[[1]]$post[[1]] :::: +```{r} +#| label: make-post-schedule + +umap_boost_post_grid <- crossing(umap_boost_grid, threshold = (5:10)/10) + +post_proc_schedule <- + umap_boost_post_grid %>% + # First collapse the submodel parameters + group_nest(neighbors, min_n, .key = "predict_stage") %>% + # Second, collapse into postprocessing loop + mutate( + predict_stage = map(predict_stage, + ~ .x %>% group_nest(trees, .key = "post_stage")) + ) %>% + # Now find the number of trees to fit + full_join( + umap_boost_grid %>% + summarize(trees = max(trees), .by = c(neighbors, min_n)), + by = c("neighbors", "min_n") + ) %>% + relocate(trees, .after = min_n) %>% + # Now collapse over the preprocessor for conditional execution + group_nest(neighbors, .key = "second_stage") +``` + + +Everything is the same as the submodel loop except for each submodel value, we have yet another nested structure to help use iterative over postprocessing parameters: + +```{r} +#| label: post-predict +post_proc_schedule %>% + pluck("second_stage") %>% + pluck(1) %>% + pluck("predict_stage") %>% + pluck(1) +``` + +and the postprocessing loop is contained in: + +```{r} +#| label: post-post +post_proc_schedule %>% + pluck("second_stage") %>% + pluck(1) %>% + pluck("predict_stage") %>% + pluck(1)%>% + pluck("post_stage") %>% + pluck(1) +``` + From 837ba1c2b624618e5c79fd1a1abb9dbdfb37b11a Mon Sep 17 00:00:00 2001 From: topepo Date: Fri, 29 Nov 2024 14:34:04 -0500 Subject: [PATCH 08/13] more nesting code --- learn/work/efficient-grid-search/index.qmd | 92 ++++++++++++++++------ 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/learn/work/efficient-grid-search/index.qmd b/learn/work/efficient-grid-search/index.qmd index 27b15faf..7e917da4 100644 --- a/learn/work/efficient-grid-search/index.qmd +++ b/learn/work/efficient-grid-search/index.qmd @@ -1,11 +1,11 @@ --- -title: "Efficient Grid Search" +title: "Algorithms for Efficient Grid Search" categories: - tuning type: learn-subsection weight: 4 description: | - A general discussion on grids and how to efficiently estimate performance. . + A general discussion of tricks for making a seemingly inefficient process very efficient. toc: true toc-depth: 2 include-after-body: ../../../resources.html @@ -32,7 +32,7 @@ This article demonstrates .... Consider it an expanded version of Section 13.5 of [_Tidy Models with R_](https://www.tmwr.org/grid-search#efficient-grids). -## The Overall Grid Tuning Procedure +## The Overall Grid Tuning Procedure {#sec-overview} tidymodels has three phases of creating a model pipeline: @@ -177,7 +177,7 @@ umap_rf_schedule umap_rf_schedule$second_stage[[1]] ``` -In general, conditional execution is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in section TODO when we can run the computations in parallel. +In general, conditional execution is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in @sec-grid-types as well as in @sec-in-parallel when we can run the computations in parallel. ## Sidebar: Types of Grids {#sec-grid-types} @@ -403,32 +403,16 @@ For the first of these, with `min_n` equal to `r submodel_schedule$second_stage[ submodel_schedule$second_stage[[1]]$predict_stage[[1]] ``` -## Types of Postprocessors {#sec-postprocessors} +## Adding Postprocessors {#sec-postprocessors} -Those needing tuning and those that are just applied. +Postprocessors are tools that adjust the predicted values produced by models. For example, for a regression model, you might want to limit the range of the predicted values to specific values. Also, we've already mentioned how we might want to adjust probability thresholds for classification models in @sec-overview. +We can treat the probability threshold as a tuning parameter to enable the model to make the most appropriate tradeoff between false negatives and false positives. -## Postprocessing Example: Probability Threshold Optimization {#sec-prob-thresholds} +One important point about this example is that no estimation step occurs. We are simply choosing a different threshold for the probability and resetting the hard class predictions. Some postprocessing methods, specifically calibration methods, need some other data set to estimate other model parameters that will then be used to adjust the original model's predictions. -```{r} -#| label: full-umap-boosting-threshold-grid -umap_boost_thrsh_grid <- crossing(umap_boost_grid, threshold = (5:10)/10) - -post_submodels <- - umap_boost_thrsh_grid %>% - group_nest(neighbors, min_n, .key = "predict_with") %>% - mutate( - predict_with = map(predict_with, ~ .x %>% group_nest(trees, .key = "post")) - ) -post_schedule <- - models_to_fit %>% - full_join(post_submodels, by = c("neighbors", "min_n")) - -post_schedule -post_schedule$predict_with[[1]] -post_schedule$predict_with[[1]]$post[[1]] -``` +Let's further torture our UMAP/boosted tree pipeline to add an additional `threshold` parameter for the probability. Let's say that we want to evaluate thresholds of 0.5, 0.6, ..., to 1.0. Unsurprisingly, this adds an additional loop inside our submodel-prediction loop (Lines 11-13 below): :::: {.columns} @@ -477,3 +461,61 @@ post_schedule$predict_with[[1]]$post[[1]] :::: +Organizing the grid data to programmatically evaluate the models, we'll add another level of nesting. + +```{r} +#| label: umap-boost-post-grid +umap_boost_post_grid <- + crossing(umap_boost_grid, threshold = (5:10)/10) + +post_proc_schedule <- + umap_boost_post_grid %>% + # First collapse the submodel parameters + group_nest(neighbors, min_n, .key = "predict_stage") %>% + # Second, collapse into postprocessing loop + mutate( + predict_stage = map(predict_stage, + ~ .x %>% group_nest(trees, .key = "post_stage")) + ) %>% + # Now find the number of trees to fit + full_join( + # This is in min_grid: + umap_boost_grid %>% + summarize(trees = max(trees), .by = c(neighbors, min_n)), + by = c("neighbors", "min_n") + ) %>% + relocate(trees, .after = min_n) %>% + # Now collapse over the preprocessor for conditional execution + group_nest(neighbors, .key = "second_stage") +``` + +To start, the loop for postprocessing is defined here: + +```{r} +#| label: umap-boost-post-umap +post_proc_schedule +``` + +Within the first iteration of that loop, we can see the non-submodel parameter(s) to iterate over (where $t=100$): + +```{r} +#| label: umap-boost-post-fits +post_proc_schedule$second_stage[[1]] +``` + +Once we fit the first combination of parameters for the boosted tree, we start predicting the sequence of submodels... + +```{r} +#| label: umap-boost-post-submodels +post_proc_schedule$second_stage[[1]]$predict_stage[[1]] +``` + +but for each of these, we have a sequence of postprocessing adjustments to make: + +```{r} +#| label: umap-boost-post-thresholds +post_proc_schedule$second_stage[[1]]$predict_stage[[1]]$post_stage[[1]] +``` + +Looking at the _five nested loops_, it would be reasonable to question whether this approach is, in fact, efficient. First, keep in mind that we are evaluating `r format(nrow(umap_boost_post_grid), big.mark = ",")` tuning parameter candidates while only training `r nrow(models_to_fit)` models. Second, the most expensive steps in this process are those that training preprocessors and models (Lines 4 and 8 above). The other loops are making predictions or adjustments and these are already efficient (and are easily vectorized). + From f267ed81f7496952779f4b944717cf2e0f81bf6a Mon Sep 17 00:00:00 2001 From: topepo Date: Fri, 29 Nov 2024 14:34:12 -0500 Subject: [PATCH 09/13] rendered files --- .../index/execute-results/html.json | 4 +- learn/index-listing.json | 2 +- learn/index.html.md | 2 - .../figs/fig-two-grids-1.svg | 171 + .../work/efficient-grid-search/index.html.md | 506 +- site_libs/Proj4Leaflet-1.0.1/proj4leaflet.js | 272 - .../crosstalk-1.2.1/css/crosstalk.min.css | 1 - site_libs/crosstalk-1.2.1/js/crosstalk.js | 1474 --- site_libs/crosstalk-1.2.1/js/crosstalk.js.map | 37 - site_libs/crosstalk-1.2.1/js/crosstalk.min.js | 2 - .../crosstalk-1.2.1/js/crosstalk.min.js.map | 1 - site_libs/crosstalk-1.2.1/scss/crosstalk.scss | 75 - .../datatables-binding-0.33/datatables.js | 1539 --- .../datatables-crosstalk.css | 32 - .../css/jquery.dataTables.extra.css | 28 - .../css/jquery.dataTables.min.css | 1 - .../js/jquery.dataTables.min.js | 4 - site_libs/htmltools-fill-0.5.8.1/fill.css | 21 - site_libs/htmlwidgets-1.6.4/htmlwidgets.js | 901 -- site_libs/jquery-3.6.0/jquery-3.6.0.js | 10881 ---------------- site_libs/jquery-3.6.0/jquery-3.6.0.min.js | 2 - site_libs/jquery-3.6.0/jquery-3.6.0.min.map | 1 - site_libs/kePrint-0.0.1/kePrint.js | 8 - site_libs/leaflet-1.3.1/images/layers-2x.png | Bin 1259 -> 0 bytes site_libs/leaflet-1.3.1/images/layers.png | Bin 696 -> 0 bytes .../leaflet-1.3.1/images/marker-icon-2x.png | Bin 2464 -> 0 bytes .../leaflet-1.3.1/images/marker-icon.png | Bin 1466 -> 0 bytes .../leaflet-1.3.1/images/marker-shadow.png | Bin 618 -> 0 bytes site_libs/leaflet-1.3.1/leaflet.css | 636 - site_libs/leaflet-1.3.1/leaflet.js | 5 - site_libs/leaflet-binding-2.2.2/leaflet.js | 2787 ---- .../leaflet-providers_2.0.0.js | 1178 -- .../leaflet-providers-plugin.js | 3 - site_libs/leafletfix-1.0.0/leafletfix.css | 36 - site_libs/lightable-0.0.1/lightable.css | 272 - .../jquery.nouislider.min.css | 4 - .../jquery.nouislider.min.js | 3 - site_libs/proj4-2.6.2/proj4.min.js | 1 - .../rstudio_leaflet-1.3.1/images/1px.png | Bin 68 -> 0 bytes .../rstudio_leaflet-1.3.1/rstudio_leaflet.css | 41 - .../selectize-0.12.0/selectize.bootstrap3.css | 401 - site_libs/selectize-0.12.0/selectize.min.js | 3 - 42 files changed, 650 insertions(+), 20685 deletions(-) create mode 100644 learn/work/efficient-grid-search/figs/fig-two-grids-1.svg delete mode 100644 site_libs/Proj4Leaflet-1.0.1/proj4leaflet.js delete mode 100644 site_libs/crosstalk-1.2.1/css/crosstalk.min.css delete mode 100644 site_libs/crosstalk-1.2.1/js/crosstalk.js delete mode 100644 site_libs/crosstalk-1.2.1/js/crosstalk.js.map delete mode 100644 site_libs/crosstalk-1.2.1/js/crosstalk.min.js delete mode 100644 site_libs/crosstalk-1.2.1/js/crosstalk.min.js.map delete mode 100644 site_libs/crosstalk-1.2.1/scss/crosstalk.scss delete mode 100644 site_libs/datatables-binding-0.33/datatables.js delete mode 100644 site_libs/datatables-css-0.0.0/datatables-crosstalk.css delete mode 100644 site_libs/dt-core-1.13.6/css/jquery.dataTables.extra.css delete mode 100644 site_libs/dt-core-1.13.6/css/jquery.dataTables.min.css delete mode 100644 site_libs/dt-core-1.13.6/js/jquery.dataTables.min.js delete mode 100644 site_libs/htmltools-fill-0.5.8.1/fill.css delete mode 100644 site_libs/htmlwidgets-1.6.4/htmlwidgets.js delete mode 100644 site_libs/jquery-3.6.0/jquery-3.6.0.js delete mode 100644 site_libs/jquery-3.6.0/jquery-3.6.0.min.js delete mode 100644 site_libs/jquery-3.6.0/jquery-3.6.0.min.map delete mode 100644 site_libs/kePrint-0.0.1/kePrint.js delete mode 100644 site_libs/leaflet-1.3.1/images/layers-2x.png delete mode 100644 site_libs/leaflet-1.3.1/images/layers.png delete mode 100644 site_libs/leaflet-1.3.1/images/marker-icon-2x.png delete mode 100644 site_libs/leaflet-1.3.1/images/marker-icon.png delete mode 100644 site_libs/leaflet-1.3.1/images/marker-shadow.png delete mode 100644 site_libs/leaflet-1.3.1/leaflet.css delete mode 100644 site_libs/leaflet-1.3.1/leaflet.js delete mode 100644 site_libs/leaflet-binding-2.2.2/leaflet.js delete mode 100644 site_libs/leaflet-providers-2.0.0/leaflet-providers_2.0.0.js delete mode 100644 site_libs/leaflet-providers-plugin-2.2.2/leaflet-providers-plugin.js delete mode 100644 site_libs/leafletfix-1.0.0/leafletfix.css delete mode 100644 site_libs/lightable-0.0.1/lightable.css delete mode 100644 site_libs/nouislider-7.0.10/jquery.nouislider.min.css delete mode 100644 site_libs/nouislider-7.0.10/jquery.nouislider.min.js delete mode 100644 site_libs/proj4-2.6.2/proj4.min.js delete mode 100644 site_libs/rstudio_leaflet-1.3.1/images/1px.png delete mode 100644 site_libs/rstudio_leaflet-1.3.1/rstudio_leaflet.css delete mode 100644 site_libs/selectize-0.12.0/selectize.bootstrap3.css delete mode 100644 site_libs/selectize-0.12.0/selectize.min.js diff --git a/_freeze/learn/work/efficient-grid-search/index/execute-results/html.json b/_freeze/learn/work/efficient-grid-search/index/execute-results/html.json index 02f4d8ea..6d9e023a 100644 --- a/_freeze/learn/work/efficient-grid-search/index/execute-results/html.json +++ b/_freeze/learn/work/efficient-grid-search/index/execute-results/html.json @@ -1,8 +1,8 @@ { - "hash": "9d5d8a21b7baca0ff971614c6a6330e7", + "hash": "677a5308d208457aa11561fe75e14004", "result": { "engine": "knitr", - "markdown": "---\ntitle: \"Efficient Grid Search\"\ncategories:\n - tuning\ntype: learn-subsection\nweight: 4\ndescription: | \n A general discussion on grids and how to efficiently estimate performance. .\ntoc: true\ntoc-depth: 2\ninclude-after-body: ../../../resources.html\n---\n\n\n\n\n\n## Introduction\n\nTo use code in this article, you will need to install the following packages: tidymodels.\n\nThis article demonstrates ....\n\n\n## The Overall Grid Tuning Procedure\n\ntidymodels has three phases of creating a model pipeline: \n\n - preprocessors: computations that prepare data for the model\n - supervised model: the actual model fit\n - postprocessing: adjustments to model predictions \n\nEach of these stages can have tuning parameters. Some examples:\n\n- We might add a spline basis expansion to a predictor to enable to have a nonlinear trend in the model. We take one predictor and create multiple columns that are based on the original column. As we add new spline columns, the model can be more complex. We don't know how many columns to add so we try different values and see which maximizes performance. \n\n- If many predictors are correlated with one another, we could determine which predictors (and how many) can be removed to reduce multicollinearity. One approach is the have a threshold for the maximum allowable pairwise correlations and have an algorithm remove the smallest set of predictors to satisfy the constraint. The threshold would need to be tuned. \n\n- Most models have tuning parameters. Boosted trees have many but two of the main parameters are the learning rate and the number of trees in the ensemble. Both usually require tuning. \n\n- For binary classification models, especially those with a class imbalance, it is possible that the default 50% probability threshold that is used to define what is \"an event\" does not satisfy the user's needs. For example, when screening blood bank samples, we want a model to err on the side of throwing out disease-free donations as long as we minimize the number of bad blood samples that truly are diseased. In other words, we might want to tune the probability threshold to achieve a high specificity metric. \n\nOnce we know what the tuning parameters are and which phases of the pipeline they are from, we can create a grid of values to evaluate. Since a pipeline can have multiple tuning parameters, a _candidate_ is defined to be a single set of actual parameter values that could be tested. A set of multiple tuning parameter candidate values is a grid. \n\nFor grid search, we evaluate each candidate in the grid by training the entire pipeline, predicting a set of data, and computing performance metrics that estimate the candidate’s efficacy. Once this is finished, we can pick the candidate set that shows the best value. \n\nThis page outlines the general process and describes different constraints and approaches to efficiently evaluating a grid of candidate values. Unexpectedly, the tools that we use to increase efficiency often result in more and more complex routines to compute performance for the grid. \n\nWe’ll break down some of the complications and exploitations that are relevant for model tuning (via grid search). We’ll also start with very simple use-cases and steadily increase the complexity of the task as well as the complexity of the solutions. \n\n## How Does Training Occur in the Pipeline?\n\nWe should describe how different models would accomplish the three stages of a model pipeline. \n\nOn one hand, there are deep neural networks. These are highly nonlinear models whose model structure is defined as a sequence of layers. The architecture of the network is defined by different types of layers that are intended to do different things. The mantra of deep learning is to add layers to the network that can accomplish all of the tasks discussed above. There is little to no delineation regarding what is a pre- or post-processor; it is all part of the model and all parameters are estimated simultaneously. We will call this type of model a “simultaneous estimation” since it all happens at the same time. \n\nOn the other hand is nearly every other type of model. In most cases, preprocessing tasks are separate estimation procedures that are executed independently of the model fit. There are cases such as principal component regression that conducts PCA signal extraction before passing the results to a routine that executes ordinary least squares. However, there are still two separate estimation tasks that are carried out in sequence, not simultaneously. We’ll call this type of training “stagewise estimation” since there are multiple, distinct training steps that occur in sequence. \n\nOur focus is 100% on stagewise estimation. \n\n## Stage-Wise Modeling and Conditional Execution\n\nOne important part of processing a grid of candidate values is to avoid repeating any computations wherever possible. This leads to the idea of conditional execution. Let’s think of an example. \n\nSuppose our pipeline consists of one preprocess and the model fit (i.e., no postprocessing). Let’s choose a fairly expensive preprocessing technique: UMAP feature extraction. UMAP has a variety of tuning parameters and one is the number of nearest neighbors to use when creating a network of nearby training set samples. Suppose that we will consider values ranging from 1 to 30. \n\nFor our model, we’ll choose a random forest model. This also has tuning parameters and we’ll choose to optimize “m-try”; the number of predictors to randomly select each time a split is created in any decision tree. \n\nFor illustration, let’s use a grid of points with six candidates: \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\nlibrary(tidymodels)\numap_rf_param <- parameters(neighbors(c(1, 30)), mtry(c(1, 100)))\numap_rf_grid <- grid_regular(umap_rf_param, levels = c(2, 3))\numap_rf_grid %>% arrange(neighbors, mtry)\n#> # A tibble: 6 × 2\n#> neighbors mtry\n#> \n#> 1 1 1\n#> 2 1 50\n#> 3 1 100\n#> 4 30 1\n#> 5 30 50\n#> 6 30 100\n```\n:::\n\n\n\nTo evaluate these candidates, we could just loop through each row, train a pipeline with that row’s candidate values, predictor a holdout set, and then compute a performance statistic. What does “train a pipeline” mean though? For the first candidate, it means \n\n- Carry out the UMAP estimation process on the training set using a single nearest neighbor. \n- Apply UMAP to the training set and save the transformed data. \n- Estimate a random forest model with `mtry = 1` using the transformed training set. \n\nThe first and third steps are two separate estimations.\n\nThe remaining tasks are to\n\n- Apply UMAP to the holdout data. \n- Predict the holdout data with the fitted random forest model. \n- Compute the performance statistic of interest (e.g., R2, accuracy, RMSE, etc.)\n\nFor the second candidate, the process is exactly the same _except_ that the random forest model uses `mtry = 50` instead of `mtry = 1`. \n\nWe _have_ to compute a new random forest model since it is a different model. However, the UMAP step is identical to the previous candidate’s preprocessor since it has the same values. Since UMAP is expensive, we end up spending a lot of time doing something that we have already done. \n\nThis would not be the case if there were no connection between the preprocessing tuning parameters and the model parameters, for example, this grid would not have repeated computations: \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```\n#> # A tibble: 6 × 2\n#> neighbors mtry\n#> \n#> 1 5 1\n#> 2 10 1\n#> 3 15 50\n#> 4 20 50\n#> 5 25 100\n#> 6 30 100\n```\n:::\n\n\n\nIn this case, the UMAP step would be different for each candidate and we would have to recompute it anyways. \n\nOnce solution to avoiding redundant computations would be to cache the UMAP computations so that they could be reused later. \n\nA better solutions is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. For the first grid of candidates contained in `umap_rf_grid`: \n\n:::: {.columns}\n\n::: {.column width=\"10%\"}\n\n:::\n\n::: {.column width=\"80%\"}\n\n\n```pseudocode\n#| html-line-number: true\n#| html-line-number-punc: \":\"\n\n\\begin{algorithm}\n\\begin{algorithmic}\n\\State $\\mathfrak{D}^{tr}$: training set\n\\State $\\mathfrak{D}^{ho}$: holdout set\n\\For{$k \\in \\{1, 30\\}$}\n \\State Using $k$ neighbors, train UMAP on $\\mathfrak{D}^{tr}$\n \\State Apply UMAP to $\\mathfrak{D}^{tr}$, creating $\\widehat{\\mathfrak{D}}^{tr}_k$\n \\State Apply UMAP to $\\mathfrak{D}^{ho}$, creating $\\widehat{\\mathfrak{D}}^{ho}_k$ \n \\For{$m \\in \\{1, 50, 100\\}$}\n \\State Train a random forest model with $m_{try} = m$ on $\\mathfrak{D}^{tr}_k$ to produce $\\widehat{f}_{km}$\n \\State Predict $\\widehat{\\mathfrak{D}}^{ho}_k$ with $\\widehat{f}_{km}$\n \\State Compute performance statistic $\\widehat{Q}_{km}$. \n \\EndFor\n\\EndFor\n\\State Determine the $k$ and $m$ calues corresponding to the best value of $\\widehat{Q}_{km}$.\n\\end{algorithmic}\n\\end{algorithm}\n```\n\n:::\n\n::: {.column width=\"10%\"}\n\n:::\n\n::::\n\n\nIn this way, we evaluated six candidates via two UMAP models and six random forest models. We avoid four redundant and expensive UMAP fits.\n\nWe can organize this data using a nested structure:\n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\numap_rf_nested_grid <- \n umap_rf_grid %>% \n group_nest(neighbors, .key = \"second_stage\")\numap_rf_nested_grid\n#> # A tibble: 2 × 2\n#> neighbors second_stage\n#> >\n#> 1 1 [3 × 1]\n#> 2 30 [3 × 1]\numap_rf_nested_grid$second_stage[[1]]\n#> # A tibble: 3 × 1\n#> mtry\n#> \n#> 1 1\n#> 2 50\n#> 3 100\n```\n:::\n\n\n\nIn general, this is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in section TODO when we can run the computations in parallel. \n\n## Sidebar: Types of Grids\n\nSince the type of grid mattered for this example, let’s go on a quick “side quest” to talk about the two major types of grids. \n\nthe effect of integers (max size etc) and also on duplicate computations\n\n## Sidebar: Parallel Processing\n\n\n\n## What are Submodels?\n\n\n## How Can we Exploit Submodels? \n\n\n## Types of Postprocessors\n\nThose needing tuning and those that are just applied. \n\n\n\n## Postprocessing Example: PRobability Threshold Optimization\n\n\n", + "markdown": "---\ntitle: \"Algorithms for Efficient Grid Search\"\ncategories:\n - tuning\ntype: learn-subsection\nweight: 4\ndescription: | \n A general discussion of tricks for making a seemingly inefficient process very efficient. \ntoc: true\ntoc-depth: 2\ninclude-after-body: ../../../resources.html\n---\n\n\n\n\n\n## Introduction\n\nTo use code in this article, you will need to install the following packages: tidymodels.\n\nThis article demonstrates ....\n\nConsider it an expanded version of Section 13.5 of [_Tidy Models with R_](https://www.tmwr.org/grid-search#efficient-grids). \n\n\n## The Overall Grid Tuning Procedure {#sec-overview}\n\ntidymodels has three phases of creating a model pipeline: \n\n - preprocessors: computations that prepare data for the model\n - supervised model: the actual model fit\n - postprocessing: adjustments to model predictions \n\nEach of these stages can have tuning parameters. Some examples:\n\n- We might add a spline basis expansion to a predictor to enable to have a nonlinear trend in the model. We take one predictor and create multiple columns that are based on the original column. As we add new spline columns, the model can be more complex. We don't know how many columns to add so we try different values and see which maximizes performance. \n\n- If many predictors are correlated with one another, we could determine which predictors (and how many) can be removed to reduce multicollinearity. One approach is the have a threshold for the maximum allowable pairwise correlations and have an algorithm remove the smallest set of predictors to satisfy the constraint. The threshold would need to be tuned. \n\n- Most models have tuning parameters. Boosted trees have many but two of the main parameters are the learning rate and the number of trees in the ensemble. Both usually require tuning. \n\n- For binary classification models, especially those with a class imbalance, it is possible that the default 50% probability threshold that is used to define what is \"an event\" does not satisfy the user's needs. For example, when screening blood bank samples, we want a model to err on the side of throwing out disease-free donations as long as we minimize the number of bad blood samples that truly are diseased. In other words, we might want to tune the probability threshold to achieve a high specificity metric. \n\nOnce we know what the tuning parameters are and which phases of the pipeline they are from, we can create a grid of values to evaluate. Since a pipeline can have multiple tuning parameters, a _candidate_ is defined to be a single set of actual parameter values that could be tested. A set of multiple tuning parameter candidate values is a grid. \n\nFor grid search, we evaluate each candidate in the grid by training the entire pipeline, predicting a set of data, and computing performance metrics that estimate the candidate’s efficacy. Once this is finished, we can pick the candidate set that shows the best value. \n\nThis page outlines the general process and describes different constraints and approaches to efficiently evaluating a grid of candidate values. Unexpectedly, the tools that we use to increase efficiency often result in more and more complex routines to compute performance for the grid. \n\nWe’ll break down some of the complications and exploitations that are relevant for model tuning (via grid search). We’ll also start with very simple use-cases and steadily increase the complexity of the task as well as the complexity of the solutions. \n\n## How Does Training Occur in the Pipeline?\n\nWe should describe how different models would accomplish the three stages of a model pipeline. \n\nOn one hand, there are deep neural networks. These are highly nonlinear models whose model structure is defined as a sequence of layers. The architecture of the network is defined by different types of layers that are intended to do different things. The mantra of deep learning is to add layers to the network that can accomplish all of the tasks discussed above. There is little to no delineation regarding what is a pre- or post-processor; it is all part of the model and all parameters are estimated simultaneously. We will call this type of model a “simultaneous estimation” since it all happens at the same time. \n\nOn the other hand is nearly every other type of model. In most cases, preprocessing tasks are separate estimation procedures that are executed independently of the model fit. There are cases such as principal component regression that conducts PCA signal extraction before passing the results to a routine that executes ordinary least squares. However, there are still two separate estimation tasks that are carried out in sequence, not simultaneously. We’ll call this type of training “stagewise estimation” since there are multiple, distinct training steps that occur in sequence. \n\nOur focus is 100% on stagewise estimation. \n\n## Stage-Wise Modeling and Conditional Execution {#sec-stagewise-exe}\n\nOne important part of processing a grid of candidate values is to avoid repeating any computations wherever possible. This leads to the idea of conditional execution. Let’s think of an example. \n\nSuppose our pipeline consists of one preprocess and the model fit (i.e., no postprocessing). Let’s choose a fairly expensive preprocessing technique: UMAP feature extraction. UMAP has a variety of tuning parameters and one is the number of nearest neighbors to use when creating a network of nearby training set samples. Suppose that we will consider values ranging from 1 to 30. \n\nFor our model, we’ll choose a random forest model. This also has tuning parameters and we’ll choose to optimize “m-try”; the number of predictors to randomly select each time a split is created in any decision tree. \n\nFor illustration, let’s use a grid of points with six candidates: \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\nlibrary(tidymodels)\numap_rf_param <- parameters(neighbors(c(1, 30)), mtry(c(1, 100)))\numap_rf_grid <- grid_regular(umap_rf_param, levels = c(2, 3))\numap_rf_grid %>% arrange(neighbors, mtry)\n#> # A tibble: 6 × 2\n#> neighbors mtry\n#> \n#> 1 1 1\n#> 2 1 50\n#> 3 1 100\n#> 4 30 1\n#> 5 30 50\n#> 6 30 100\n```\n:::\n\n\n\nTo evaluate these candidates, we could just loop through each row, train a pipeline with that row’s candidate values, predictor a holdout set, and then compute a performance statistic. What does “train a pipeline” mean though? For the first candidate, it means \n\n- Carry out the UMAP estimation process on the training set using a single nearest neighbor. \n- Apply UMAP to the training set and save the transformed data. \n- Estimate a random forest model with $m_{try} = 1$ using the transformed training set. \n\nThe first and third steps are two separate estimations.\n\nThe remaining tasks are to\n\n- Apply UMAP to the holdout data. \n- Predict the holdout data with the fitted random forest model. \n- Compute the performance statistic of interest (e.g., $R^2$, accuracy, RMSE, etc.)\n\nFor the second candidate, the process is exactly the same _except_ that the random forest model uses $m_{try} = 50$ instead of $m_{try} = 1$. \n\nWe _have_ to compute a new random forest model since it is a different model. However, the UMAP step is identical to the previous candidate’s preprocessor since it has the same values. Since UMAP is expensive, we end up spending a lot of time doing something that we have already done. \n\nThis would not be the case if there were no connection between the preprocessing tuning parameters and the model parameters, for example, this grid would not have repeated computations: \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```\n#> # A tibble: 6 × 2\n#> neighbors mtry\n#> \n#> 1 5 1\n#> 2 10 1\n#> 3 15 50\n#> 4 20 50\n#> 5 25 100\n#> 6 30 100\n```\n:::\n\n\n\nIn this case, the UMAP step would be different for each candidate so there is no computational redundancy to eliminate. \n\nOnce solution to avoiding redundant computations would be to cache the UMAP computations so that they could be reused later. \n\nA better solution is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. Here's a listing of this algorithm: \n\n:::: {.columns}\n\n::: {.column width=\"10%\"}\n\n:::\n\n::: {.column width=\"80%\"}\n\n\n```pseudocode\n#| html-line-number: true\n#| html-line-number-punc: \":\"\n\n\\begin{algorithm}\n\\begin{algorithmic}\n\\State $\\mathfrak{D}^{fit}$: training set\n\\State $\\mathfrak{D}^{pred}$: holdout set\n\\For{$k \\in \\{1, 30\\}$}\n \\State Using $k$ neighbors, train UMAP on $\\mathfrak{D}^{fit}$\n \\State Apply UMAP to $\\mathfrak{D}^{fit}$, creating $\\widehat{\\mathfrak{D}}^{fit}_k$\n \\State Apply UMAP to $\\mathfrak{D}^{pred}$, creating $\\widehat{\\mathfrak{D}}^{pred}_k$ \n \\For{$m \\in \\{1, 50, 100\\}$}\n \\State Train a random forest model with $m_{try} = m$ on $\\mathfrak{D}^{fit}_k$ to produce $\\widehat{f}_{km}$\n \\State Predict $\\widehat{\\mathfrak{D}}^{pred}_k$ with $\\widehat{f}_{k,m}$\n \\State Compute performance statistic $\\widehat{Q}_{k,m}$. \n \\EndFor\n\\EndFor\n\\State Determine the $k$ and $m$ values corresponding to the best value of $\\widehat{Q}_{k,m}$.\n\\end{algorithmic}\n\\end{algorithm}\n```\n\n:::\n\n::: {.column width=\"10%\"}\n\n:::\n\n::::\n\n\nIn this way, we evaluated six candidates via two UMAP models and six random forest models. We avoid four redundant and expensive UMAP fits.\n\nWe can organize this data using a nested structure:\n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\numap_rf_schedule <- \n umap_rf_grid %>% \n group_nest(neighbors, .key = \"second_stage\")\n\n# The training loop to iterate over:\numap_rf_schedule\n#> # A tibble: 2 × 2\n#> neighbors second_stage\n#> >\n#> 1 1 [3 × 1]\n#> 2 30 [3 × 1]\n\n# Within that loop, iterative over these:\numap_rf_schedule$second_stage[[1]]\n#> # A tibble: 3 × 1\n#> mtry\n#> \n#> 1 1\n#> 2 50\n#> 3 100\n```\n:::\n\n\n\nIn general, conditional execution is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in @sec-grid-types as well as in @sec-in-parallel when we can run the computations in parallel. \n\n## Sidebar: Types of Grids {#sec-grid-types}\n\nSince the type of grid mattered for this example, let’s go on a quick “side quest” to talk about the two major types of grids. \n\nWhen we think about parameter grids, there are two types: regular and irregular. A regular grid is one where we create a univariate sequence of values for each tuning parameter and then create all possible combinations. Irregular grids can be made in many different ways but cannot be combinatorial in nature. Examples are random grids (i.e., simulating random values across parameter ranges), latin hypercube designs, and others. The best irregular grids are _space-filling designs_; they attempt to cover the entire parameter space and try to distribute the points so that none are overly similar to the others. \n\nHere’s an example of a regular grid and a space-filling design where each has 15 candidate points: \n\n\n\n::: {.cell layout-align=\"center\"}\n::: {.cell-output-display}\n![](figs/fig-two-grids-1.svg){#fig-two-grids fig-align='center' width=95%}\n:::\n:::\n\n\n\nSpace-filling designs are, on average, the best approach for parameter tuning. They need far fewer grid points to cover the parameter space than regular grids. \n\nThe choice of grid types is relevant here because it affects the tactics used to efficiently process the grid. Consider the two grids above. Our conditional execution approach works well for the regular grid; for each value of the number of neighbors, there is a nice vector of values for the other parameter. Conversely, for the space-filling design, there is only a single corresponding penalty value for each unique number of neighbors. No conditional execution is possible. \n\n## There's Something We Didn't Tell You {#sec-add-in-resampling}\n\nOne important aspect of grid search that we have obfuscated in the algorithm is shown in the previous section. Line 10 says “Compute performance statistic $\\widehat{Q}_{k,n,t}$” which left a lot out. \n\nIn that same algorithm, we had two data sets: a training set ($\\mathfrak{D}^{fit}$) and a holdout set ($\\mathfrak{D}^{pred}$) of data points that were not included in the training set. We said “holdout” instead of “test set” for generality. \n\nWhen tuning models, a single holdout set (a.k.a. a validation set) or multiple holdout sets are used (via resampling). For example, with 10-fold cross-validation, there is _another_ loop over the number resamples. \n\n\n:::: {.columns}\n\n::: {.column width=\"10%\"}\n\n:::\n\n::: {.column width=\"80%\"}\n\n\n```pseudocode\n#| html-line-number: true\n#| html-line-number-punc: \":\"\n\n\\begin{algorithm}\n\\begin{algorithmic}\n\\For{$b = 1, \\ldots, B$}\n \\State Split the data into $\\mathfrak{D}^{fit}_b$ and $\\mathfrak{D}^{pred}_b$\n \\For{$k \\in \\{1, 30\\}$}\n \\State Using $k$ neighbors, train UMAP on $\\mathfrak{D}^{fit}_b$\n \\State Apply UMAP to $\\mathfrak{D}^{fit}_b$, creating $\\widehat{\\mathfrak{D}}^{fit}_{b,k}$\n \\State Apply UMAP to $\\mathfrak{D}^{pred}_b$, creating $\\widehat{\\mathfrak{D}}^{pred}_{b,k}$ \n \\For{$m \\in \\{1, 50, 100\\}$}\n \\State Train a random forest model with $m_{try} = m$ on $\\mathfrak{D}^{fit}_{b,k}$ to produce $\\widehat{f}_{b,k,m}$\n \\State Predict $\\widehat{\\mathfrak{D}}^{pred}_{b,k}$ with $\\widehat{f}_{b,k,m}$\n \\State Compute performance statistic $\\widehat{Q}_{b,k,m}$. \n \\EndFor\n \\EndFor\n\\EndFor \n\\State Compute the average performance metric $\\widehat{Q}_{k,m} = 1/B\\sum \\widehat{Q}_{b,k,m}$\n\\State Determine the $k$ and $m$ values corresponding to the best value of $\\widehat{Q}_{k,m}$.\n\\end{algorithmic}\n\\end{algorithm}\n```\n\n:::\n\n::: {.column width=\"10%\"}\n\n:::\n\n::::\n\n\n\n## Sidebar: Parallel Processing {#sec-in-parallel}\n\nAnother tool to increase computational efficiency is parallel processing. We have multiple computational cores on our computers that can perform calculations simultaneously. If we have $B$ resamples and $s$ candidates in our grid, there are $B \\times s$ model fits to evaluate and none of them depend on the others^[That’s true but only _kind of true_; we’ll elaborate in @sec-submodels below.]. We can split up these operations into chunks and send these chunks to different computer cores. \n\nThis is discussed at leangth in Section 13.5.2 of [_Tidy Models with R_](https://www.tmwr.org/grid-search#parallel-processing). Here are the highlights: \n \n- You can almost always reduce the time to process a grid using parallelism. \n- How the tasks are chunked can matter a lot. For example: \n\t- If you have a lot of data and I/O is expensive, you might want to chunk the data so that all the computations use the same data. For example, for resampling, you might send all tasks for $\\mathfrak{D}^{fit}_b$ and $\\mathfrak{D}^{pred}_b$ are on the same computer core. \n- If your preprocessing (if any) is very fast, you may want to flatten the loops so that the computational tasks loop over each of the $B \\times s$ resample/candidate combinations. This means that the preprocessing will be recalculated several times but, if they are cheap, this might be the fasted approach. \n\n\n## What are Submodels? {#sec-submodels}\n\n“Submodels” is a situation where a model can make predictions for different tuning parameter values _without_ retraining. Two examples: \n\n- _Boosting_ ensembles take the same model and, using different case weights, create a sequential set of fits. For example, a tree ensemble created with 100 boosting iterations will contain 100 trees, each depending on the previous fits. For many implementations, you can fit the largest ensemble size (100 in this example) and, for free, make predictions on sizes 1 through 99. \n- Some regularized models, such as glmnet, have a penalization parameter that attenuates the magnitude of the regression coefficients (think weight decay in neural networks). We often tune over this parameter. For glmnet, the model simultaneously creates a _path_ of possible penalty values. With a single model fit, you can predict on _any_ penalty values. \n\nNot all models contain parameters that can use the “submodel trick.” For example, the random forest model from @sec-stagewise-exe is also an ensemble of trees. However, those trees are not created in a sequence; they are all independent of one another. \n\nHowever, when our model has submodel parameters, there can be a massive efficient gain with grid search. \n\n## How Can we Exploit Submodels? {#sec-submodel-processing}\n\nLet's use boosting as an example. We'll repeat our UMAP preprocessor and tune over: \n\n - The number of boosted trees in an ensemble with values 1 through 100. \n - The minimum number of data points required to keep splitting the tree (aka `min_n`) for values of 1, 20, and 40. \n\nWith the same UMAP grid, let’s use is $2 \\times 100 \\times 3 = 600$ grid points.\n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\numap_boost_param <- parameters(neighbors(c(1, 30)), min_n(c(1, 40)), trees(c(1, 100)))\numap_boost_grid <- grid_regular(umap_boost_param, levels = c(2, 3, 100))\numap_boost_grid\n#> # A tibble: 600 × 3\n#> neighbors min_n trees\n#> \n#> 1 1 1 1\n#> 2 30 1 1\n#> 3 1 20 1\n#> 4 30 20 1\n#> 5 1 40 1\n#> 6 30 40 1\n#> 7 1 1 2\n#> 8 30 1 2\n#> 9 1 20 2\n#> 10 30 20 2\n#> # ℹ 590 more rows\n```\n:::\n\n\n\nThere are six unique combinations of the number of neighbors and `min_n`, so we can evaluate only six models using these parameters: \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\nmodels_to_fit <- \n umap_boost_grid %>% \n summarize(trees = max(trees), .by = c(neighbors, min_n))\nmodels_to_fit\n#> # A tibble: 6 × 3\n#> neighbors min_n trees\n#> \n#> 1 1 1 100\n#> 2 30 1 100\n#> 3 1 20 100\n#> 4 30 20 100\n#> 5 1 40 100\n#> 6 30 40 100\n```\n:::\n\n\n\nWith submodels, our algorithm has yet another nested loop in Lines 9-11:\n\n:::: {.columns}\n\n::: {.column width=\"10%\"}\n\n:::\n\n::: {.column width=\"80%\"}\n\n\n```pseudocode\n#| html-line-number: true\n#| html-line-number-punc: \":\"\n\n\\begin{algorithm}\n\\begin{algorithmic}\n\\For{$b = 1, \\ldots, B$}\n \\State Split the data into $\\mathfrak{D}^{fit}_b$ and $\\mathfrak{D}^{pred}_b$\n \\For{$k \\in \\{1, 30\\}$}\n \\State Using $k$ neighbors, train UMAP on $\\mathfrak{D}^{fit}_b$\n \\State Apply UMAP to $\\mathfrak{D}^{fit}_b$, creating $\\widehat{\\mathfrak{D}}^{fit}_{b,k}$\n \\State Apply UMAP to $\\mathfrak{D}^{pred}_b$, creating $\\widehat{\\mathfrak{D}}^{pred}_{b,k}$ \n \\For{$n \\in \\{1, 20, 40\\}$}\n \\State Train a boosting model with $n_{min}= n$ and 100 trees on $\\mathfrak{D}^{fit}_{b,k}$ to produce $\\widehat{f}_{b,k,n,100}$\n \\For{$t \\in \\{1, 2, \\ldots, 100\\}$}\n \\State Predict $\\widehat{\\mathfrak{D}}^{pred}_{b,k}$ with $\\widehat{f}_{b,k,n,100}$ using at $t$ trees\n \\State Compute performance statistic $\\widehat{Q}_{b,k,n,t}$. \n \\EndFor\n \\EndFor\n \\EndFor\n\\EndFor\n\\State Compute the average performance metric $\\widehat{Q}_{k,n,t} = 1/B\\sum \\widehat{Q}_{b,k,n,t}$\n\\State Determine the $k$, $n$, and $t$ values corresponding to the best value of $\\widehat{Q}_{k,n,t}$.\n\\end{algorithmic}\n\\end{algorithm}\n```\n\n:::\n\n::: {.column width=\"10%\"}\n\n:::\n\n::::\n\nThis added complexity has huge benefits in efficiency since our loop for the number of trees ($t$) is for _predicting_ and not for _training_.\n\n\n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\nsubmodel_schedule <- \n umap_boost_grid %>% \n # First collapse the submodel parameters\n group_nest(neighbors, min_n, .key = \"predict_stage\") %>% \n # Now find the number of trees to fit\n full_join(\n umap_boost_grid %>% \n summarize(trees = max(trees), .by = c(neighbors, min_n)),\n by = c(\"neighbors\", \"min_n\")\n ) %>% \n relocate(trees, .after = min_n) %>% \n # Now collapse over the preprocessor for conditional execution\n group_nest(neighbors, .key = \"second_stage\")\n\nsubmodel_schedule\n#> # A tibble: 2 × 2\n#> neighbors second_stage\n#> >\n#> 1 1 [3 × 3]\n#> 2 30 [3 × 3]\n```\n:::\n\n\n\nLet's look at one iteration of the loop. For the model with a single neighbor, what is the first boosted tree that we fit? \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\nsubmodel_schedule$second_stage[[1]]\n#> # A tibble: 3 × 3\n#> min_n trees predict_stage\n#> >\n#> 1 1 100 [100 × 1]\n#> 2 20 100 [100 × 1]\n#> 3 40 100 [100 × 1]\n```\n:::\n\n\n\nFor the first of these, with `min_n` equal to 1 and 100 trees, what is the set of tree sizes that we should predict? \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\nsubmodel_schedule$second_stage[[1]]$predict_stage[[1]]\n#> # A tibble: 100 × 1\n#> trees\n#> \n#> 1 1\n#> 2 2\n#> 3 3\n#> 4 4\n#> 5 5\n#> 6 6\n#> 7 7\n#> 8 8\n#> 9 9\n#> 10 10\n#> # ℹ 90 more rows\n```\n:::\n\n\n\n## Adding Postprocessors {#sec-postprocessors}\n\nPostprocessors are tools that adjust the predicted values produced by models. For example, for a regression model, you might want to limit the range of the predicted values to specific values. Also, we've already mentioned how we might want to adjust probability thresholds for classification models in @sec-overview. \n\nWe can treat the probability threshold as a tuning parameter to enable the model to make the most appropriate tradeoff between false negatives and false positives.\n\nOne important point about this example is that no estimation step occurs. We are simply choosing a different threshold for the probability and resetting the hard class predictions. Some postprocessing methods, specifically calibration methods, need some other data set to estimate other model parameters that will then be used to adjust the original model's predictions. \n\n\nLet's further torture our UMAP/boosted tree pipeline to add an additional `threshold` parameter for the probability. Let's say that we want to evaluate thresholds of 0.5, 0.6, ..., to 1.0. Unsurprisingly, this adds an additional loop inside our submodel-prediction loop (Lines 11-13 below): \n\n:::: {.columns}\n\n::: {.column width=\"10%\"}\n\n:::\n\n::: {.column width=\"80%\"}\n\n\n```pseudocode\n#| html-line-number: true\n#| html-line-number-punc: \":\"\n\n\\begin{algorithm}\n\\begin{algorithmic}\n\\For{$b = 1, \\ldots, B$}\n \\State Split the data into $\\mathfrak{D}^{fit}_b$ and $\\mathfrak{D}^{pred}_b$\n \\For{$k \\in \\{1, 30\\}$}\n \\State Using $k$ neighbors, train UMAP on $\\mathfrak{D}^{fit}_b$\n \\State Apply UMAP to $\\mathfrak{D}^{fit}_b$, creating $\\widehat{\\mathfrak{D}}^{fit}_{b,k}$\n \\State Apply UMAP to $\\mathfrak{D}^{pred}_b$, creating $\\widehat{\\mathfrak{D}}^{pred}_{b,k}$ \n \\For{$n \\in \\{1, 20, 40\\}$}\n \\State Train a boosting model with $n_{min}= n$ and 100 trees on $\\mathfrak{D}^{fit}_{b,k}$ to produce $\\widehat{f}_{b,k,n,100}$\n \\For{$t \\in \\{1, 2, \\ldots, 100\\}$}\n \\State Predict $\\widehat{\\mathfrak{D}}^{pred}_{b,k}$ with $\\widehat{f}_{b,k,n,100}$ using at $t$ trees\n \\For{$\\pi \\in \\{0.5, 0.6, \\ldots, 1.0\\}$}\n \\State Recompute hard class predictions with threshold $\\pi$.\n \\State Compute performance statistic $\\widehat{Q}_{b,k,n,t,\\pi}$. \n \\EndFor\n \\EndFor\n \\EndFor\n \\EndFor\n\\EndFor\n\\State Compute the average performance metric $\\widehat{Q}_{k,n,t,\\pi} = 1/B\\sum \\widehat{Q}_{b,k,n,t,\\pi}$\n\\State Determine the $k$, $n$, $t$, and $\\pi$ values corresponding to the best value of $\\widehat{Q}_{k,n,t,\\pi}$.\n\\end{algorithmic}\n\\end{algorithm}\n```\n\n:::\n\n::: {.column width=\"10%\"}\n\n:::\n\n::::\n\nOrganizing the grid data to programmatically evaluate the models, we'll add another level of nesting. \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\numap_boost_post_grid <-\n crossing(umap_boost_grid, threshold = (5:10)/10)\n\npost_proc_schedule <-\n umap_boost_post_grid %>%\n # First collapse the submodel parameters\n group_nest(neighbors, min_n, .key = \"predict_stage\") %>%\n # Second, collapse into postprocessing loop\n mutate(\n predict_stage = map(predict_stage,\n ~ .x %>% group_nest(trees, .key = \"post_stage\"))\n ) %>%\n # Now find the number of trees to fit\n full_join(\n # This is in min_grid:\n umap_boost_grid %>%\n summarize(trees = max(trees), .by = c(neighbors, min_n)),\n by = c(\"neighbors\", \"min_n\")\n ) %>%\n relocate(trees, .after = min_n) %>% \n # Now collapse over the preprocessor for conditional execution\n group_nest(neighbors, .key = \"second_stage\")\n```\n:::\n\n\n\nTo start, the loop for postprocessing is defined here: \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\npost_proc_schedule\n#> # A tibble: 2 × 2\n#> neighbors second_stage\n#> >\n#> 1 1 [3 × 3]\n#> 2 30 [3 × 3]\n```\n:::\n\n\n\nWithin the first iteration of that loop, we can see the non-submodel parameter(s) to iterate over (where $t=100$):\n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\npost_proc_schedule$second_stage[[1]]\n#> # A tibble: 3 × 3\n#> min_n trees predict_stage \n#> \n#> 1 1 100 \n#> 2 20 100 \n#> 3 40 100 \n```\n:::\n\n\n\nOnce we fit the first combination of parameters for the boosted tree, we start predicting the sequence of submodels...\n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\npost_proc_schedule$second_stage[[1]]$predict_stage[[1]]\n#> # A tibble: 100 × 2\n#> trees post_stage\n#> >\n#> 1 1 [6 × 1]\n#> 2 2 [6 × 1]\n#> 3 3 [6 × 1]\n#> 4 4 [6 × 1]\n#> 5 5 [6 × 1]\n#> 6 6 [6 × 1]\n#> 7 7 [6 × 1]\n#> 8 8 [6 × 1]\n#> 9 9 [6 × 1]\n#> 10 10 [6 × 1]\n#> # ℹ 90 more rows\n```\n:::\n\n\n\nbut for each of these, we have a sequence of postprocessing adjustments to make: \n\n\n\n::: {.cell layout-align=\"center\"}\n\n```{.r .cell-code}\npost_proc_schedule$second_stage[[1]]$predict_stage[[1]]$post_stage[[1]]\n#> # A tibble: 6 × 1\n#> threshold\n#> \n#> 1 0.5\n#> 2 0.6\n#> 3 0.7\n#> 4 0.8\n#> 5 0.9\n#> 6 1\n```\n:::\n\n\n\nLooking at the _five nested loops_, it would be reasonable to question whether this approach is, in fact, efficient. First, keep in mind that we are evaluating 3,600 tuning parameter candidates while only training 6 models. Second, the most expensive steps in this process are those that training preprocessors and models (Lines 4 and 8 above). The other loops are making predictions or adjustments and these are already efficient (and are easily vectorized).\n\n", "supporting": [], "filters": [ "rmarkdown/pagebreak.lua" diff --git a/learn/index-listing.json b/learn/index-listing.json index e01b34c4..081fb773 100644 --- a/learn/index-listing.json +++ b/learn/index-listing.json @@ -3,6 +3,7 @@ "items": [ "/start/case-study/index.html", "/learn/statistics/survival-metrics-details/index.html", + "/learn/work/efficient-grid-search/index.html", "/learn/models/calibration/index.html", "/learn/work/fairness-detectors/index.html", "/learn/statistics/bootstrap/index.html", @@ -16,7 +17,6 @@ "/learn/work/case-weights/index.html", "/learn/develop/metrics/index.html", "/learn/statistics/survival-metrics/index.html", - "/learn/work/efficient-grid-search/index.html", "/start/resampling/index.html", "/learn/work/fairness-readmission/index.html", "/learn/statistics/survival-case-study/index.html", diff --git a/learn/index.html.md b/learn/index.html.md index b6924ce6..19c0b80b 100644 --- a/learn/index.html.md +++ b/learn/index.html.md @@ -19,7 +19,5 @@ listing: - - After you know [what you need to get started](/start/) with tidymodels, you can learn more and go further. Find articles here to help you solve specific problems using the tidymodels framework. diff --git a/learn/work/efficient-grid-search/figs/fig-two-grids-1.svg b/learn/work/efficient-grid-search/figs/fig-two-grids-1.svg new file mode 100644 index 00000000..399e5bdf --- /dev/null +++ b/learn/work/efficient-grid-search/figs/fig-two-grids-1.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Regular + + + + + + + + + + +Space-Filling + + + + + +1e-08 +1e-05 +1e-02 + + + +1e-08 +1e-05 +1e-02 +0 +10 +20 +30 +40 +50 + + + + + + +penalty +neighbors + + diff --git a/learn/work/efficient-grid-search/index.html.md b/learn/work/efficient-grid-search/index.html.md index c044012f..dbda79d0 100644 --- a/learn/work/efficient-grid-search/index.html.md +++ b/learn/work/efficient-grid-search/index.html.md @@ -1,11 +1,11 @@ --- -title: "Efficient Grid Search" +title: "Algorithms for Efficient Grid Search" categories: - tuning type: learn-subsection weight: 4 description: | - A general discussion on grids and how to efficiently estimate performance. . + A general discussion of tricks for making a seemingly inefficient process very efficient. toc: true toc-depth: 2 include-after-body: ../../../resources.html @@ -22,8 +22,10 @@ To use code in this article, you will need to install the following packages: t This article demonstrates .... +Consider it an expanded version of Section 13.5 of [_Tidy Models with R_](https://www.tmwr.org/grid-search#efficient-grids). -## The Overall Grid Tuning Procedure + +## The Overall Grid Tuning Procedure {#sec-overview} tidymodels has three phases of creating a model pipeline: @@ -59,7 +61,7 @@ On the other hand is nearly every other type of model. In most cases, preprocess Our focus is 100% on stagewise estimation. -## Stage-Wise Modeling and Conditional Execution +## Stage-Wise Modeling and Conditional Execution {#sec-stagewise-exe} One important part of processing a grid of candidate values is to avoid repeating any computations wherever possible. This leads to the idea of conditional execution. Let’s think of an example. @@ -96,7 +98,7 @@ To evaluate these candidates, we could just loop through each row, train a pipel - Carry out the UMAP estimation process on the training set using a single nearest neighbor. - Apply UMAP to the training set and save the transformed data. -- Estimate a random forest model with `mtry = 1` using the transformed training set. +- Estimate a random forest model with $m_{try} = 1$ using the transformed training set. The first and third steps are two separate estimations. @@ -104,9 +106,9 @@ The remaining tasks are to - Apply UMAP to the holdout data. - Predict the holdout data with the fitted random forest model. -- Compute the performance statistic of interest (e.g., R2, accuracy, RMSE, etc.) +- Compute the performance statistic of interest (e.g., $R^2$, accuracy, RMSE, etc.) -For the second candidate, the process is exactly the same _except_ that the random forest model uses `mtry = 50` instead of `mtry = 1`. +For the second candidate, the process is exactly the same _except_ that the random forest model uses $m_{try} = 50$ instead of $m_{try} = 1$. We _have_ to compute a new random forest model since it is a different model. However, the UMAP step is identical to the previous candidate’s preprocessor since it has the same values. Since UMAP is expensive, we end up spending a lot of time doing something that we have already done. @@ -131,11 +133,11 @@ This would not be the case if there were no connection between the preprocessing -In this case, the UMAP step would be different for each candidate and we would have to recompute it anyways. +In this case, the UMAP step would be different for each candidate so there is no computational redundancy to eliminate. Once solution to avoiding redundant computations would be to cache the UMAP computations so that they could be reused later. -A better solutions is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. For the first grid of candidates contained in `umap_rf_grid`: +A better solution is _conditional execution_. In this case, we determine the unique set of candidate values associated with the preprocessor and loop over these. Within each loop, we we train and predict the random forest model across its candidate values. Here's a listing of this algorithm: :::: {.columns} @@ -152,19 +154,19 @@ A better solutions is _conditional execution_. In this case, we determine the un \begin{algorithm} \begin{algorithmic} -\State $\mathfrak{D}^{tr}$: training set -\State $\mathfrak{D}^{ho}$: holdout set +\State $\mathfrak{D}^{fit}$: training set +\State $\mathfrak{D}^{pred}$: holdout set \For{$k \in \{1, 30\}$} - \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{tr}$ - \State Apply UMAP to $\mathfrak{D}^{tr}$, creating $\widehat{\mathfrak{D}}^{tr}_k$ - \State Apply UMAP to $\mathfrak{D}^{ho}$, creating $\widehat{\mathfrak{D}}^{ho}_k$ + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}$ + \State Apply UMAP to $\mathfrak{D}^{fit}$, creating $\widehat{\mathfrak{D}}^{fit}_k$ + \State Apply UMAP to $\mathfrak{D}^{pred}$, creating $\widehat{\mathfrak{D}}^{pred}_k$ \For{$m \in \{1, 50, 100\}$} - \State Train a random forest model with $m_{try} = m$ on $\mathfrak{D}^{tr}_k$ to produce $\widehat{f}_{km}$ - \State Predict $\widehat{\mathfrak{D}}^{ho}_k$ with $\widehat{f}_{km}$ - \State Compute performance statistic $\widehat{Q}_{km}$. + \State Train a random forest model with $m_{try} = m$ on $\mathfrak{D}^{fit}_k$ to produce $\widehat{f}_{km}$ + \State Predict $\widehat{\mathfrak{D}}^{pred}_k$ with $\widehat{f}_{k,m}$ + \State Compute performance statistic $\widehat{Q}_{k,m}$. \EndFor \EndFor -\State Determine the $k$ and $m$ calues corresponding to the best value of $\widehat{Q}_{km}$. +\State Determine the $k$ and $m$ values corresponding to the best value of $\widehat{Q}_{k,m}$. \end{algorithmic} \end{algorithm} ``` @@ -187,16 +189,20 @@ We can organize this data using a nested structure: ::: {.cell layout-align="center"} ```{.r .cell-code} -umap_rf_nested_grid <- +umap_rf_schedule <- umap_rf_grid %>% group_nest(neighbors, .key = "second_stage") -umap_rf_nested_grid + +# The training loop to iterate over: +umap_rf_schedule #> # A tibble: 2 × 2 #> neighbors second_stage #> > #> 1 1 [3 × 1] #> 2 30 [3 × 1] -umap_rf_nested_grid$second_stage[[1]] + +# Within that loop, iterative over these: +umap_rf_schedule$second_stage[[1]] #> # A tibble: 3 × 1 #> mtry #> @@ -208,30 +214,470 @@ umap_rf_nested_grid$second_stage[[1]] -In general, this is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in section TODO when we can run the computations in parallel. +In general, conditional execution is a good idea. Even when the second grid is used, there is no computational loss incurred by conditional execution. This is also true if the preprocessing technique is inexpensive. We will see one issue with conditional execution that comes up in @sec-grid-types as well as in @sec-in-parallel when we can run the computations in parallel. -## Sidebar: Types of Grids +## Sidebar: Types of Grids {#sec-grid-types} Since the type of grid mattered for this example, let’s go on a quick “side quest” to talk about the two major types of grids. -the effect of integers (max size etc) and also on duplicate computations +When we think about parameter grids, there are two types: regular and irregular. A regular grid is one where we create a univariate sequence of values for each tuning parameter and then create all possible combinations. Irregular grids can be made in many different ways but cannot be combinatorial in nature. Examples are random grids (i.e., simulating random values across parameter ranges), latin hypercube designs, and others. The best irregular grids are _space-filling designs_; they attempt to cover the entire parameter space and try to distribute the points so that none are overly similar to the others. + +Here’s an example of a regular grid and a space-filling design where each has 15 candidate points: + + + +::: {.cell layout-align="center"} +::: {.cell-output-display} +![](figs/fig-two-grids-1.svg){#fig-two-grids fig-align='center' width=95%} +::: +::: + + + +Space-filling designs are, on average, the best approach for parameter tuning. They need far fewer grid points to cover the parameter space than regular grids. + +The choice of grid types is relevant here because it affects the tactics used to efficiently process the grid. Consider the two grids above. Our conditional execution approach works well for the regular grid; for each value of the number of neighbors, there is a nice vector of values for the other parameter. Conversely, for the space-filling design, there is only a single corresponding penalty value for each unique number of neighbors. No conditional execution is possible. + +## There's Something We Didn't Tell You {#sec-add-in-resampling} -## Sidebar: Parallel Processing +One important aspect of grid search that we have obfuscated in the algorithm is shown in the previous section. Line 10 says “Compute performance statistic $\widehat{Q}_{k,n,t}$” which left a lot out. +In that same algorithm, we had two data sets: a training set ($\mathfrak{D}^{fit}$) and a holdout set ($\mathfrak{D}^{pred}$) of data points that were not included in the training set. We said “holdout” instead of “test set” for generality. +When tuning models, a single holdout set (a.k.a. a validation set) or multiple holdout sets are used (via resampling). For example, with 10-fold cross-validation, there is _another_ loop over the number resamples. -## What are Submodels? +:::: {.columns} + +::: {.column width="10%"} -## How Can we Exploit Submodels? +::: +::: {.column width="80%"} -## Types of Postprocessors -Those needing tuning and those that are just applied. +```pseudocode +#| html-line-number: true +#| html-line-number-punc: ":" + +\begin{algorithm} +\begin{algorithmic} +\For{$b = 1, \ldots, B$} + \State Split the data into $\mathfrak{D}^{fit}_b$ and $\mathfrak{D}^{pred}_b$ + \For{$k \in \{1, 30\}$} + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}_b$ + \State Apply UMAP to $\mathfrak{D}^{fit}_b$, creating $\widehat{\mathfrak{D}}^{fit}_{b,k}$ + \State Apply UMAP to $\mathfrak{D}^{pred}_b$, creating $\widehat{\mathfrak{D}}^{pred}_{b,k}$ + \For{$m \in \{1, 50, 100\}$} + \State Train a random forest model with $m_{try} = m$ on $\mathfrak{D}^{fit}_{b,k}$ to produce $\widehat{f}_{b,k,m}$ + \State Predict $\widehat{\mathfrak{D}}^{pred}_{b,k}$ with $\widehat{f}_{b,k,m}$ + \State Compute performance statistic $\widehat{Q}_{b,k,m}$. + \EndFor + \EndFor +\EndFor +\State Compute the average performance metric $\widehat{Q}_{k,m} = 1/B\sum \widehat{Q}_{b,k,m}$ +\State Determine the $k$ and $m$ values corresponding to the best value of $\widehat{Q}_{k,m}$. +\end{algorithmic} +\end{algorithm} +``` +::: + +::: {.column width="10%"} + +::: + +:::: + + + +## Sidebar: Parallel Processing {#sec-in-parallel} + +Another tool to increase computational efficiency is parallel processing. We have multiple computational cores on our computers that can perform calculations simultaneously. If we have $B$ resamples and $s$ candidates in our grid, there are $B \times s$ model fits to evaluate and none of them depend on the others^[That’s true but only _kind of true_; we’ll elaborate in @sec-submodels below.]. We can split up these operations into chunks and send these chunks to different computer cores. + +This is discussed at leangth in Section 13.5.2 of [_Tidy Models with R_](https://www.tmwr.org/grid-search#parallel-processing). Here are the highlights: + +- You can almost always reduce the time to process a grid using parallelism. +- How the tasks are chunked can matter a lot. For example: + - If you have a lot of data and I/O is expensive, you might want to chunk the data so that all the computations use the same data. For example, for resampling, you might send all tasks for $\mathfrak{D}^{fit}_b$ and $\mathfrak{D}^{pred}_b$ are on the same computer core. +- If your preprocessing (if any) is very fast, you may want to flatten the loops so that the computational tasks loop over each of the $B \times s$ resample/candidate combinations. This means that the preprocessing will be recalculated several times but, if they are cheap, this might be the fasted approach. + + +## What are Submodels? {#sec-submodels} + +“Submodels” is a situation where a model can make predictions for different tuning parameter values _without_ retraining. Two examples: + +- _Boosting_ ensembles take the same model and, using different case weights, create a sequential set of fits. For example, a tree ensemble created with 100 boosting iterations will contain 100 trees, each depending on the previous fits. For many implementations, you can fit the largest ensemble size (100 in this example) and, for free, make predictions on sizes 1 through 99. +- Some regularized models, such as glmnet, have a penalization parameter that attenuates the magnitude of the regression coefficients (think weight decay in neural networks). We often tune over this parameter. For glmnet, the model simultaneously creates a _path_ of possible penalty values. With a single model fit, you can predict on _any_ penalty values. + +Not all models contain parameters that can use the “submodel trick.” For example, the random forest model from @sec-stagewise-exe is also an ensemble of trees. However, those trees are not created in a sequence; they are all independent of one another. + +However, when our model has submodel parameters, there can be a massive efficient gain with grid search. + +## How Can we Exploit Submodels? {#sec-submodel-processing} + +Let's use boosting as an example. We'll repeat our UMAP preprocessor and tune over: + + - The number of boosted trees in an ensemble with values 1 through 100. + - The minimum number of data points required to keep splitting the tree (aka `min_n`) for values of 1, 20, and 40. + +With the same UMAP grid, let’s use is $2 \times 100 \times 3 = 600$ grid points. + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +umap_boost_param <- parameters(neighbors(c(1, 30)), min_n(c(1, 40)), trees(c(1, 100))) +umap_boost_grid <- grid_regular(umap_boost_param, levels = c(2, 3, 100)) +umap_boost_grid +#> # A tibble: 600 × 3 +#> neighbors min_n trees +#> +#> 1 1 1 1 +#> 2 30 1 1 +#> 3 1 20 1 +#> 4 30 20 1 +#> 5 1 40 1 +#> 6 30 40 1 +#> 7 1 1 2 +#> 8 30 1 2 +#> 9 1 20 2 +#> 10 30 20 2 +#> # ℹ 590 more rows +``` +::: + + + +There are six unique combinations of the number of neighbors and `min_n`, so we can evaluate only six models using these parameters: + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +models_to_fit <- + umap_boost_grid %>% + summarize(trees = max(trees), .by = c(neighbors, min_n)) +models_to_fit +#> # A tibble: 6 × 3 +#> neighbors min_n trees +#> +#> 1 1 1 100 +#> 2 30 1 100 +#> 3 1 20 100 +#> 4 30 20 100 +#> 5 1 40 100 +#> 6 30 40 100 +``` +::: + + + +With submodels, our algorithm has yet another nested loop in Lines 9-11: + +:::: {.columns} + +::: {.column width="10%"} + +::: + +::: {.column width="80%"} + + +```pseudocode +#| html-line-number: true +#| html-line-number-punc: ":" + +\begin{algorithm} +\begin{algorithmic} +\For{$b = 1, \ldots, B$} + \State Split the data into $\mathfrak{D}^{fit}_b$ and $\mathfrak{D}^{pred}_b$ + \For{$k \in \{1, 30\}$} + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}_b$ + \State Apply UMAP to $\mathfrak{D}^{fit}_b$, creating $\widehat{\mathfrak{D}}^{fit}_{b,k}$ + \State Apply UMAP to $\mathfrak{D}^{pred}_b$, creating $\widehat{\mathfrak{D}}^{pred}_{b,k}$ + \For{$n \in \{1, 20, 40\}$} + \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{fit}_{b,k}$ to produce $\widehat{f}_{b,k,n,100}$ + \For{$t \in \{1, 2, \ldots, 100\}$} + \State Predict $\widehat{\mathfrak{D}}^{pred}_{b,k}$ with $\widehat{f}_{b,k,n,100}$ using at $t$ trees + \State Compute performance statistic $\widehat{Q}_{b,k,n,t}$. + \EndFor + \EndFor + \EndFor +\EndFor +\State Compute the average performance metric $\widehat{Q}_{k,n,t} = 1/B\sum \widehat{Q}_{b,k,n,t}$ +\State Determine the $k$, $n$, and $t$ values corresponding to the best value of $\widehat{Q}_{k,n,t}$. +\end{algorithmic} +\end{algorithm} +``` + +::: + +::: {.column width="10%"} + +::: + +:::: + +This added complexity has huge benefits in efficiency since our loop for the number of trees ($t$) is for _predicting_ and not for _training_. + + + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +submodel_schedule <- + umap_boost_grid %>% + # First collapse the submodel parameters + group_nest(neighbors, min_n, .key = "predict_stage") %>% + # Now find the number of trees to fit + full_join( + umap_boost_grid %>% + summarize(trees = max(trees), .by = c(neighbors, min_n)), + by = c("neighbors", "min_n") + ) %>% + relocate(trees, .after = min_n) %>% + # Now collapse over the preprocessor for conditional execution + group_nest(neighbors, .key = "second_stage") + +submodel_schedule +#> # A tibble: 2 × 2 +#> neighbors second_stage +#> > +#> 1 1 [3 × 3] +#> 2 30 [3 × 3] +``` +::: + + + +Let's look at one iteration of the loop. For the model with a single neighbor, what is the first boosted tree that we fit? + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +submodel_schedule$second_stage[[1]] +#> # A tibble: 3 × 3 +#> min_n trees predict_stage +#> > +#> 1 1 100 [100 × 1] +#> 2 20 100 [100 × 1] +#> 3 40 100 [100 × 1] +``` +::: + + + +For the first of these, with `min_n` equal to 1 and 100 trees, what is the set of tree sizes that we should predict? + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +submodel_schedule$second_stage[[1]]$predict_stage[[1]] +#> # A tibble: 100 × 1 +#> trees +#> +#> 1 1 +#> 2 2 +#> 3 3 +#> 4 4 +#> 5 5 +#> 6 6 +#> 7 7 +#> 8 8 +#> 9 9 +#> 10 10 +#> # ℹ 90 more rows +``` +::: + + + +## Adding Postprocessors {#sec-postprocessors} + +Postprocessors are tools that adjust the predicted values produced by models. For example, for a regression model, you might want to limit the range of the predicted values to specific values. Also, we've already mentioned how we might want to adjust probability thresholds for classification models in @sec-overview. + +We can treat the probability threshold as a tuning parameter to enable the model to make the most appropriate tradeoff between false negatives and false positives. + +One important point about this example is that no estimation step occurs. We are simply choosing a different threshold for the probability and resetting the hard class predictions. Some postprocessing methods, specifically calibration methods, need some other data set to estimate other model parameters that will then be used to adjust the original model's predictions. + + +Let's further torture our UMAP/boosted tree pipeline to add an additional `threshold` parameter for the probability. Let's say that we want to evaluate thresholds of 0.5, 0.6, ..., to 1.0. Unsurprisingly, this adds an additional loop inside our submodel-prediction loop (Lines 11-13 below): + +:::: {.columns} + +::: {.column width="10%"} + +::: + +::: {.column width="80%"} + + +```pseudocode +#| html-line-number: true +#| html-line-number-punc: ":" + +\begin{algorithm} +\begin{algorithmic} +\For{$b = 1, \ldots, B$} + \State Split the data into $\mathfrak{D}^{fit}_b$ and $\mathfrak{D}^{pred}_b$ + \For{$k \in \{1, 30\}$} + \State Using $k$ neighbors, train UMAP on $\mathfrak{D}^{fit}_b$ + \State Apply UMAP to $\mathfrak{D}^{fit}_b$, creating $\widehat{\mathfrak{D}}^{fit}_{b,k}$ + \State Apply UMAP to $\mathfrak{D}^{pred}_b$, creating $\widehat{\mathfrak{D}}^{pred}_{b,k}$ + \For{$n \in \{1, 20, 40\}$} + \State Train a boosting model with $n_{min}= n$ and 100 trees on $\mathfrak{D}^{fit}_{b,k}$ to produce $\widehat{f}_{b,k,n,100}$ + \For{$t \in \{1, 2, \ldots, 100\}$} + \State Predict $\widehat{\mathfrak{D}}^{pred}_{b,k}$ with $\widehat{f}_{b,k,n,100}$ using at $t$ trees + \For{$\pi \in \{0.5, 0.6, \ldots, 1.0\}$} + \State Recompute hard class predictions with threshold $\pi$. + \State Compute performance statistic $\widehat{Q}_{b,k,n,t,\pi}$. + \EndFor + \EndFor + \EndFor + \EndFor +\EndFor +\State Compute the average performance metric $\widehat{Q}_{k,n,t,\pi} = 1/B\sum \widehat{Q}_{b,k,n,t,\pi}$ +\State Determine the $k$, $n$, $t$, and $\pi$ values corresponding to the best value of $\widehat{Q}_{k,n,t,\pi}$. +\end{algorithmic} +\end{algorithm} +``` + +::: + +::: {.column width="10%"} + +::: + +:::: + +Organizing the grid data to programmatically evaluate the models, we'll add another level of nesting. + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +umap_boost_post_grid <- + crossing(umap_boost_grid, threshold = (5:10)/10) + +post_proc_schedule <- + umap_boost_post_grid %>% + # First collapse the submodel parameters + group_nest(neighbors, min_n, .key = "predict_stage") %>% + # Second, collapse into postprocessing loop + mutate( + predict_stage = map(predict_stage, + ~ .x %>% group_nest(trees, .key = "post_stage")) + ) %>% + # Now find the number of trees to fit + full_join( + # This is in min_grid: + umap_boost_grid %>% + summarize(trees = max(trees), .by = c(neighbors, min_n)), + by = c("neighbors", "min_n") + ) %>% + relocate(trees, .after = min_n) %>% + # Now collapse over the preprocessor for conditional execution + group_nest(neighbors, .key = "second_stage") +``` +::: + + + +To start, the loop for postprocessing is defined here: + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +post_proc_schedule +#> # A tibble: 2 × 2 +#> neighbors second_stage +#> > +#> 1 1 [3 × 3] +#> 2 30 [3 × 3] +``` +::: + + + +Within the first iteration of that loop, we can see the non-submodel parameter(s) to iterate over (where $t=100$): + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +post_proc_schedule$second_stage[[1]] +#> # A tibble: 3 × 3 +#> min_n trees predict_stage +#> +#> 1 1 100 +#> 2 20 100 +#> 3 40 100 +``` +::: + + + +Once we fit the first combination of parameters for the boosted tree, we start predicting the sequence of submodels... + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +post_proc_schedule$second_stage[[1]]$predict_stage[[1]] +#> # A tibble: 100 × 2 +#> trees post_stage +#> > +#> 1 1 [6 × 1] +#> 2 2 [6 × 1] +#> 3 3 [6 × 1] +#> 4 4 [6 × 1] +#> 5 5 [6 × 1] +#> 6 6 [6 × 1] +#> 7 7 [6 × 1] +#> 8 8 [6 × 1] +#> 9 9 [6 × 1] +#> 10 10 [6 × 1] +#> # ℹ 90 more rows +``` +::: + + + +but for each of these, we have a sequence of postprocessing adjustments to make: + + + +::: {.cell layout-align="center"} + +```{.r .cell-code} +post_proc_schedule$second_stage[[1]]$predict_stage[[1]]$post_stage[[1]] +#> # A tibble: 6 × 1 +#> threshold +#> +#> 1 0.5 +#> 2 0.6 +#> 3 0.7 +#> 4 0.8 +#> 5 0.9 +#> 6 1 +``` +::: -## Postprocessing Example: PRobability Threshold Optimization +Looking at the _five nested loops_, it would be reasonable to question whether this approach is, in fact, efficient. First, keep in mind that we are evaluating 3,600 tuning parameter candidates while only training 6 models. Second, the most expensive steps in this process are those that training preprocessors and models (Lines 4 and 8 above). The other loops are making predictions or adjustments and these are already efficient (and are easily vectorized). diff --git a/site_libs/Proj4Leaflet-1.0.1/proj4leaflet.js b/site_libs/Proj4Leaflet-1.0.1/proj4leaflet.js deleted file mode 100644 index eaa650c1..00000000 --- a/site_libs/Proj4Leaflet-1.0.1/proj4leaflet.js +++ /dev/null @@ -1,272 +0,0 @@ -(function (factory) { - var L, proj4; - if (typeof define === 'function' && define.amd) { - // AMD - define(['leaflet', 'proj4'], factory); - } else if (typeof module === 'object' && typeof module.exports === "object") { - // Node/CommonJS - L = require('leaflet'); - proj4 = require('proj4'); - module.exports = factory(L, proj4); - } else { - // Browser globals - if (typeof window.L === 'undefined' || typeof window.proj4 === 'undefined') - throw 'Leaflet and proj4 must be loaded first'; - factory(window.L, window.proj4); - } -}(function (L, proj4) { - if (proj4.__esModule && proj4.default) { - // If proj4 was bundled as an ES6 module, unwrap it to get - // to the actual main proj4 object. - // See discussion in https://github.com/kartena/Proj4Leaflet/pull/147 - proj4 = proj4.default; - } - - L.Proj = {}; - - L.Proj._isProj4Obj = function(a) { - return (typeof a.inverse !== 'undefined' && - typeof a.forward !== 'undefined'); - }; - - L.Proj.Projection = L.Class.extend({ - initialize: function(code, def, bounds) { - var isP4 = L.Proj._isProj4Obj(code); - this._proj = isP4 ? code : this._projFromCodeDef(code, def); - this.bounds = isP4 ? def : bounds; - }, - - project: function (latlng) { - var point = this._proj.forward([latlng.lng, latlng.lat]); - return new L.Point(point[0], point[1]); - }, - - unproject: function (point, unbounded) { - var point2 = this._proj.inverse([point.x, point.y]); - return new L.LatLng(point2[1], point2[0], unbounded); - }, - - _projFromCodeDef: function(code, def) { - if (def) { - proj4.defs(code, def); - } else if (proj4.defs[code] === undefined) { - var urn = code.split(':'); - if (urn.length > 3) { - code = urn[urn.length - 3] + ':' + urn[urn.length - 1]; - } - if (proj4.defs[code] === undefined) { - throw 'No projection definition for code ' + code; - } - } - - return proj4(code); - } - }); - - L.Proj.CRS = L.Class.extend({ - includes: L.CRS, - - options: { - transformation: new L.Transformation(1, 0, -1, 0) - }, - - initialize: function(a, b, c) { - var code, - proj, - def, - options; - - if (L.Proj._isProj4Obj(a)) { - proj = a; - code = proj.srsCode; - options = b || {}; - - this.projection = new L.Proj.Projection(proj, options.bounds); - } else { - code = a; - def = b; - options = c || {}; - this.projection = new L.Proj.Projection(code, def, options.bounds); - } - - L.Util.setOptions(this, options); - this.code = code; - this.transformation = this.options.transformation; - - if (this.options.origin) { - this.transformation = - new L.Transformation(1, -this.options.origin[0], - -1, this.options.origin[1]); - } - - if (this.options.scales) { - this._scales = this.options.scales; - } else if (this.options.resolutions) { - this._scales = []; - for (var i = this.options.resolutions.length - 1; i >= 0; i--) { - if (this.options.resolutions[i]) { - this._scales[i] = 1 / this.options.resolutions[i]; - } - } - } - - this.infinite = !this.options.bounds; - - }, - - scale: function(zoom) { - var iZoom = Math.floor(zoom), - baseScale, - nextScale, - scaleDiff, - zDiff; - if (zoom === iZoom) { - return this._scales[zoom]; - } else { - // Non-integer zoom, interpolate - baseScale = this._scales[iZoom]; - nextScale = this._scales[iZoom + 1]; - scaleDiff = nextScale - baseScale; - zDiff = (zoom - iZoom); - return baseScale + scaleDiff * zDiff; - } - }, - - zoom: function(scale) { - // Find closest number in this._scales, down - var downScale = this._closestElement(this._scales, scale), - downZoom = this._scales.indexOf(downScale), - nextScale, - nextZoom, - scaleDiff; - // Check if scale is downScale => return array index - if (scale === downScale) { - return downZoom; - } - if (downScale === undefined) { - return -Infinity; - } - // Interpolate - nextZoom = downZoom + 1; - nextScale = this._scales[nextZoom]; - if (nextScale === undefined) { - return Infinity; - } - scaleDiff = nextScale - downScale; - return (scale - downScale) / scaleDiff + downZoom; - }, - - distance: L.CRS.Earth.distance, - - R: L.CRS.Earth.R, - - /* Get the closest lowest element in an array */ - _closestElement: function(array, element) { - var low; - for (var i = array.length; i--;) { - if (array[i] <= element && (low === undefined || low < array[i])) { - low = array[i]; - } - } - return low; - } - }); - - L.Proj.GeoJSON = L.GeoJSON.extend({ - initialize: function(geojson, options) { - this._callLevel = 0; - L.GeoJSON.prototype.initialize.call(this, geojson, options); - }, - - addData: function(geojson) { - var crs; - - if (geojson) { - if (geojson.crs && geojson.crs.type === 'name') { - crs = new L.Proj.CRS(geojson.crs.properties.name); - } else if (geojson.crs && geojson.crs.type) { - crs = new L.Proj.CRS(geojson.crs.type + ':' + geojson.crs.properties.code); - } - - if (crs !== undefined) { - this.options.coordsToLatLng = function(coords) { - var point = L.point(coords[0], coords[1]); - return crs.projection.unproject(point); - }; - } - } - - // Base class' addData might call us recursively, but - // CRS shouldn't be cleared in that case, since CRS applies - // to the whole GeoJSON, inluding sub-features. - this._callLevel++; - try { - L.GeoJSON.prototype.addData.call(this, geojson); - } finally { - this._callLevel--; - if (this._callLevel === 0) { - delete this.options.coordsToLatLng; - } - } - } - }); - - L.Proj.geoJson = function(geojson, options) { - return new L.Proj.GeoJSON(geojson, options); - }; - - L.Proj.ImageOverlay = L.ImageOverlay.extend({ - initialize: function (url, bounds, options) { - L.ImageOverlay.prototype.initialize.call(this, url, null, options); - this._projectedBounds = bounds; - }, - - // Danger ahead: Overriding internal methods in Leaflet. - // Decided to do this rather than making a copy of L.ImageOverlay - // and doing very tiny modifications to it. - // Future will tell if this was wise or not. - _animateZoom: function (event) { - var scale = this._map.getZoomScale(event.zoom); - var northWest = L.point(this._projectedBounds.min.x, this._projectedBounds.max.y); - var offset = this._projectedToNewLayerPoint(northWest, event.zoom, event.center); - - L.DomUtil.setTransform(this._image, offset, scale); - }, - - _reset: function () { - var zoom = this._map.getZoom(); - var pixelOrigin = this._map.getPixelOrigin(); - var bounds = L.bounds( - this._transform(this._projectedBounds.min, zoom)._subtract(pixelOrigin), - this._transform(this._projectedBounds.max, zoom)._subtract(pixelOrigin) - ); - var size = bounds.getSize(); - - L.DomUtil.setPosition(this._image, bounds.min); - this._image.style.width = size.x + 'px'; - this._image.style.height = size.y + 'px'; - }, - - _projectedToNewLayerPoint: function (point, zoom, center) { - var viewHalf = this._map.getSize()._divideBy(2); - var newTopLeft = this._map.project(center, zoom)._subtract(viewHalf)._round(); - var topLeft = newTopLeft.add(this._map._getMapPanePos()); - - return this._transform(point, zoom)._subtract(topLeft); - }, - - _transform: function (point, zoom) { - var crs = this._map.options.crs; - var transformation = crs.transformation; - var scale = crs.scale(zoom); - - return transformation.transform(point, scale); - } - }); - - L.Proj.imageOverlay = function (url, bounds, options) { - return new L.Proj.ImageOverlay(url, bounds, options); - }; - - return L.Proj; -})); diff --git a/site_libs/crosstalk-1.2.1/css/crosstalk.min.css b/site_libs/crosstalk-1.2.1/css/crosstalk.min.css deleted file mode 100644 index 6b453828..00000000 --- a/site_libs/crosstalk-1.2.1/css/crosstalk.min.css +++ /dev/null @@ -1 +0,0 @@ -.container-fluid.crosstalk-bscols{margin-left:-30px;margin-right:-30px;white-space:normal}body>.container-fluid.crosstalk-bscols{margin-left:auto;margin-right:auto}.crosstalk-input-checkboxgroup .crosstalk-options-group .crosstalk-options-column{display:inline-block;padding-right:12px;vertical-align:top}@media only screen and (max-width: 480px){.crosstalk-input-checkboxgroup .crosstalk-options-group .crosstalk-options-column{display:block;padding-right:inherit}}.crosstalk-input{margin-bottom:15px}.crosstalk-input .control-label{margin-bottom:0;vertical-align:middle}.crosstalk-input input[type="checkbox"]{margin:4px 0 0;margin-top:1px;line-height:normal}.crosstalk-input .checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.crosstalk-input .checkbox>label{padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.crosstalk-input .checkbox input[type="checkbox"],.crosstalk-input .checkbox-inline input[type="checkbox"]{position:absolute;margin-top:2px;margin-left:-20px}.crosstalk-input .checkbox+.checkbox{margin-top:-5px}.crosstalk-input .checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.crosstalk-input .checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px} diff --git a/site_libs/crosstalk-1.2.1/js/crosstalk.js b/site_libs/crosstalk-1.2.1/js/crosstalk.js deleted file mode 100644 index fd9eb53d..00000000 --- a/site_libs/crosstalk-1.2.1/js/crosstalk.js +++ /dev/null @@ -1,1474 +0,0 @@ -(function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o b) { - return 1; - } -} - -/** - * @private - */ - -var FilterSet = function () { - function FilterSet() { - _classCallCheck(this, FilterSet); - - this.reset(); - } - - _createClass(FilterSet, [{ - key: "reset", - value: function reset() { - // Key: handle ID, Value: array of selected keys, or null - this._handles = {}; - // Key: key string, Value: count of handles that include it - this._keys = {}; - this._value = null; - this._activeHandles = 0; - } - }, { - key: "update", - value: function update(handleId, keys) { - if (keys !== null) { - keys = keys.slice(0); // clone before sorting - keys.sort(naturalComparator); - } - - var _diffSortedLists = (0, _util.diffSortedLists)(this._handles[handleId], keys), - added = _diffSortedLists.added, - removed = _diffSortedLists.removed; - - this._handles[handleId] = keys; - - for (var i = 0; i < added.length; i++) { - this._keys[added[i]] = (this._keys[added[i]] || 0) + 1; - } - for (var _i = 0; _i < removed.length; _i++) { - this._keys[removed[_i]]--; - } - - this._updateValue(keys); - } - - /** - * @param {string[]} keys Sorted array of strings that indicate - * a superset of possible keys. - * @private - */ - - }, { - key: "_updateValue", - value: function _updateValue() { - var keys = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this._allKeys; - - var handleCount = Object.keys(this._handles).length; - if (handleCount === 0) { - this._value = null; - } else { - this._value = []; - for (var i = 0; i < keys.length; i++) { - var count = this._keys[keys[i]]; - if (count === handleCount) { - this._value.push(keys[i]); - } - } - } - } - }, { - key: "clear", - value: function clear(handleId) { - if (typeof this._handles[handleId] === "undefined") { - return; - } - - var keys = this._handles[handleId]; - if (!keys) { - keys = []; - } - - for (var i = 0; i < keys.length; i++) { - this._keys[keys[i]]--; - } - delete this._handles[handleId]; - - this._updateValue(); - } - }, { - key: "value", - get: function get() { - return this._value; - } - }, { - key: "_allKeys", - get: function get() { - var allKeys = Object.keys(this._keys); - allKeys.sort(naturalComparator); - return allKeys; - } - }]); - - return FilterSet; -}(); - -exports.default = FilterSet; - -},{"./util":11}],4:[function(require,module,exports){ -(function (global){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -exports.default = group; - -var _var2 = require("./var"); - -var _var3 = _interopRequireDefault(_var2); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -// Use a global so that multiple copies of crosstalk.js can be loaded and still -// have groups behave as singletons across all copies. -global.__crosstalk_groups = global.__crosstalk_groups || {}; -var groups = global.__crosstalk_groups; - -function group(groupName) { - if (groupName && typeof groupName === "string") { - if (!groups.hasOwnProperty(groupName)) { - groups[groupName] = new Group(groupName); - } - return groups[groupName]; - } else if ((typeof groupName === "undefined" ? "undefined" : _typeof(groupName)) === "object" && groupName._vars && groupName.var) { - // Appears to already be a group object - return groupName; - } else if (Array.isArray(groupName) && groupName.length == 1 && typeof groupName[0] === "string") { - return group(groupName[0]); - } else { - throw new Error("Invalid groupName argument"); - } -} - -var Group = function () { - function Group(name) { - _classCallCheck(this, Group); - - this.name = name; - this._vars = {}; - } - - _createClass(Group, [{ - key: "var", - value: function _var(name) { - if (!name || typeof name !== "string") { - throw new Error("Invalid var name"); - } - - if (!this._vars.hasOwnProperty(name)) this._vars[name] = new _var3.default(this, name); - return this._vars[name]; - } - }, { - key: "has", - value: function has(name) { - if (!name || typeof name !== "string") { - throw new Error("Invalid var name"); - } - - return this._vars.hasOwnProperty(name); - } - }]); - - return Group; -}(); - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) - -},{"./var":12}],5:[function(require,module,exports){ -(function (global){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _group = require("./group"); - -var _group2 = _interopRequireDefault(_group); - -var _selection = require("./selection"); - -var _filter = require("./filter"); - -var _input = require("./input"); - -require("./input_selectize"); - -require("./input_checkboxgroup"); - -require("./input_slider"); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var defaultGroup = (0, _group2.default)("default"); - -function var_(name) { - return defaultGroup.var(name); -} - -function has(name) { - return defaultGroup.has(name); -} - -if (global.Shiny) { - global.Shiny.addCustomMessageHandler("update-client-value", function (message) { - if (typeof message.group === "string") { - (0, _group2.default)(message.group).var(message.name).set(message.value); - } else { - var_(message.name).set(message.value); - } - }); -} - -var crosstalk = { - group: _group2.default, - var: var_, - has: has, - SelectionHandle: _selection.SelectionHandle, - FilterHandle: _filter.FilterHandle, - bind: _input.bind -}; - -/** - * @namespace crosstalk - */ -exports.default = crosstalk; - -global.crosstalk = crosstalk; - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) - -},{"./filter":2,"./group":4,"./input":6,"./input_checkboxgroup":7,"./input_selectize":8,"./input_slider":9,"./selection":10}],6:[function(require,module,exports){ -(function (global){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.register = register; -exports.bind = bind; -var $ = global.jQuery; - -var bindings = {}; - -function register(reg) { - bindings[reg.className] = reg; - if (global.document && global.document.readyState !== "complete") { - $(function () { - bind(); - }); - } else if (global.document) { - setTimeout(bind, 100); - } -} - -function bind() { - Object.keys(bindings).forEach(function (className) { - var binding = bindings[className]; - $("." + binding.className).not(".crosstalk-input-bound").each(function (i, el) { - bindInstance(binding, el); - }); - }); -} - -// Escape jQuery identifier -function $escape(val) { - return val.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, "\\$1"); -} - -function bindEl(el) { - var $el = $(el); - Object.keys(bindings).forEach(function (className) { - if ($el.hasClass(className) && !$el.hasClass("crosstalk-input-bound")) { - var binding = bindings[className]; - bindInstance(binding, el); - } - }); -} - -function bindInstance(binding, el) { - var jsonEl = $(el).find("script[type='application/json'][data-for='" + $escape(el.id) + "']"); - var data = JSON.parse(jsonEl[0].innerText); - - var instance = binding.factory(el, data); - $(el).data("crosstalk-instance", instance); - $(el).addClass("crosstalk-input-bound"); -} - -if (global.Shiny) { - var inputBinding = new global.Shiny.InputBinding(); - var _$ = global.jQuery; - _$.extend(inputBinding, { - find: function find(scope) { - return _$(scope).find(".crosstalk-input"); - }, - initialize: function initialize(el) { - if (!_$(el).hasClass("crosstalk-input-bound")) { - bindEl(el); - } - }, - getId: function getId(el) { - return el.id; - }, - getValue: function getValue(el) {}, - setValue: function setValue(el, value) {}, - receiveMessage: function receiveMessage(el, data) {}, - subscribe: function subscribe(el, callback) { - _$(el).data("crosstalk-instance").resume(); - }, - unsubscribe: function unsubscribe(el) { - _$(el).data("crosstalk-instance").suspend(); - } - }); - global.Shiny.inputBindings.register(inputBinding, "crosstalk.inputBinding"); -} - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) - -},{}],7:[function(require,module,exports){ -(function (global){ -"use strict"; - -var _input = require("./input"); - -var input = _interopRequireWildcard(_input); - -var _filter = require("./filter"); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -var $ = global.jQuery; - -input.register({ - className: "crosstalk-input-checkboxgroup", - - factory: function factory(el, data) { - /* - * map: {"groupA": ["keyA", "keyB", ...], ...} - * group: "ct-groupname" - */ - var ctHandle = new _filter.FilterHandle(data.group); - - var lastKnownKeys = void 0; - var $el = $(el); - $el.on("change", "input[type='checkbox']", function () { - var checked = $el.find("input[type='checkbox']:checked"); - if (checked.length === 0) { - lastKnownKeys = null; - ctHandle.clear(); - } else { - var keys = {}; - checked.each(function () { - data.map[this.value].forEach(function (key) { - keys[key] = true; - }); - }); - var keyArray = Object.keys(keys); - keyArray.sort(); - lastKnownKeys = keyArray; - ctHandle.set(keyArray); - } - }); - - return { - suspend: function suspend() { - ctHandle.clear(); - }, - resume: function resume() { - if (lastKnownKeys) ctHandle.set(lastKnownKeys); - } - }; - } -}); - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) - -},{"./filter":2,"./input":6}],8:[function(require,module,exports){ -(function (global){ -"use strict"; - -var _input = require("./input"); - -var input = _interopRequireWildcard(_input); - -var _util = require("./util"); - -var util = _interopRequireWildcard(_util); - -var _filter = require("./filter"); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -var $ = global.jQuery; - -input.register({ - className: "crosstalk-input-select", - - factory: function factory(el, data) { - /* - * items: {value: [...], label: [...]} - * map: {"groupA": ["keyA", "keyB", ...], ...} - * group: "ct-groupname" - */ - - var first = [{ value: "", label: "(All)" }]; - var items = util.dataframeToD3(data.items); - var opts = { - options: first.concat(items), - valueField: "value", - labelField: "label", - searchField: "label" - }; - - var select = $(el).find("select")[0]; - - var selectize = $(select).selectize(opts)[0].selectize; - - var ctHandle = new _filter.FilterHandle(data.group); - - var lastKnownKeys = void 0; - selectize.on("change", function () { - if (selectize.items.length === 0) { - lastKnownKeys = null; - ctHandle.clear(); - } else { - var keys = {}; - selectize.items.forEach(function (group) { - data.map[group].forEach(function (key) { - keys[key] = true; - }); - }); - var keyArray = Object.keys(keys); - keyArray.sort(); - lastKnownKeys = keyArray; - ctHandle.set(keyArray); - } - }); - - return { - suspend: function suspend() { - ctHandle.clear(); - }, - resume: function resume() { - if (lastKnownKeys) ctHandle.set(lastKnownKeys); - } - }; - } -}); - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) - -},{"./filter":2,"./input":6,"./util":11}],9:[function(require,module,exports){ -(function (global){ -"use strict"; - -var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); - -var _input = require("./input"); - -var input = _interopRequireWildcard(_input); - -var _filter = require("./filter"); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -var $ = global.jQuery; -var strftime = global.strftime; - -input.register({ - className: "crosstalk-input-slider", - - factory: function factory(el, data) { - /* - * map: {"groupA": ["keyA", "keyB", ...], ...} - * group: "ct-groupname" - */ - var ctHandle = new _filter.FilterHandle(data.group); - - var opts = {}; - var $el = $(el).find("input"); - var dataType = $el.data("data-type"); - var timeFormat = $el.data("time-format"); - var round = $el.data("round"); - var timeFormatter = void 0; - - // Set up formatting functions - if (dataType === "date") { - timeFormatter = strftime.utc(); - opts.prettify = function (num) { - return timeFormatter(timeFormat, new Date(num)); - }; - } else if (dataType === "datetime") { - var timezone = $el.data("timezone"); - if (timezone) timeFormatter = strftime.timezone(timezone);else timeFormatter = strftime; - - opts.prettify = function (num) { - return timeFormatter(timeFormat, new Date(num)); - }; - } else if (dataType === "number") { - if (typeof round !== "undefined") opts.prettify = function (num) { - var factor = Math.pow(10, round); - return Math.round(num * factor) / factor; - }; - } - - $el.ionRangeSlider(opts); - - function getValue() { - var result = $el.data("ionRangeSlider").result; - - // Function for converting numeric value from slider to appropriate type. - var convert = void 0; - var dataType = $el.data("data-type"); - if (dataType === "date") { - convert = function convert(val) { - return formatDateUTC(new Date(+val)); - }; - } else if (dataType === "datetime") { - convert = function convert(val) { - // Convert ms to s - return +val / 1000; - }; - } else { - convert = function convert(val) { - return +val; - }; - } - - if ($el.data("ionRangeSlider").options.type === "double") { - return [convert(result.from), convert(result.to)]; - } else { - return convert(result.from); - } - } - - var lastKnownKeys = null; - - $el.on("change.crosstalkSliderInput", function (event) { - if (!$el.data("updating") && !$el.data("animating")) { - var _getValue = getValue(), - _getValue2 = _slicedToArray(_getValue, 2), - from = _getValue2[0], - to = _getValue2[1]; - - var keys = []; - for (var i = 0; i < data.values.length; i++) { - var val = data.values[i]; - if (val >= from && val <= to) { - keys.push(data.keys[i]); - } - } - keys.sort(); - ctHandle.set(keys); - lastKnownKeys = keys; - } - }); - - // let $el = $(el); - // $el.on("change", "input[type="checkbox"]", function() { - // let checked = $el.find("input[type="checkbox"]:checked"); - // if (checked.length === 0) { - // ctHandle.clear(); - // } else { - // let keys = {}; - // checked.each(function() { - // data.map[this.value].forEach(function(key) { - // keys[key] = true; - // }); - // }); - // let keyArray = Object.keys(keys); - // keyArray.sort(); - // ctHandle.set(keyArray); - // } - // }); - - return { - suspend: function suspend() { - ctHandle.clear(); - }, - resume: function resume() { - if (lastKnownKeys) ctHandle.set(lastKnownKeys); - } - }; - } -}); - -// Convert a number to a string with leading zeros -function padZeros(n, digits) { - var str = n.toString(); - while (str.length < digits) { - str = "0" + str; - }return str; -} - -// Given a Date object, return a string in yyyy-mm-dd format, using the -// UTC date. This may be a day off from the date in the local time zone. -function formatDateUTC(date) { - if (date instanceof Date) { - return date.getUTCFullYear() + "-" + padZeros(date.getUTCMonth() + 1, 2) + "-" + padZeros(date.getUTCDate(), 2); - } else { - return null; - } -} - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) - -},{"./filter":2,"./input":6}],10:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.SelectionHandle = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _events = require("./events"); - -var _events2 = _interopRequireDefault(_events); - -var _group = require("./group"); - -var _group2 = _interopRequireDefault(_group); - -var _util = require("./util"); - -var util = _interopRequireWildcard(_util); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -/** - * Use this class to read and write (and listen for changes to) the selection - * for a Crosstalk group. This is intended to be used for linked brushing. - * - * If two (or more) `SelectionHandle` instances in the same webpage share the - * same group name, they will share the same state. Setting the selection using - * one `SelectionHandle` instance will result in the `value` property instantly - * changing across the others, and `"change"` event listeners on all instances - * (including the one that initiated the sending) will fire. - * - * @param {string} [group] - The name of the Crosstalk group, or if none, - * null or undefined (or any other falsy value). This can be changed later - * via the [SelectionHandle#setGroup](#setGroup) method. - * @param {Object} [extraInfo] - An object whose properties will be copied to - * the event object whenever an event is emitted. - */ -var SelectionHandle = exports.SelectionHandle = function () { - function SelectionHandle() { - var group = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - var extraInfo = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; - - _classCallCheck(this, SelectionHandle); - - this._eventRelay = new _events2.default(); - this._emitter = new util.SubscriptionTracker(this._eventRelay); - - // Name of the group we're currently tracking, if any. Can change over time. - this._group = null; - // The Var we're currently tracking, if any. Can change over time. - this._var = null; - // The event handler subscription we currently have on var.on("change"). - this._varOnChangeSub = null; - - this._extraInfo = util.extend({ sender: this }, extraInfo); - - this.setGroup(group); - } - - /** - * Changes the Crosstalk group membership of this SelectionHandle. The group - * being switched away from (if any) will not have its selection value - * modified as a result of calling `setGroup`, even if this handle was the - * most recent handle to set the selection of the group. - * - * The group being switched to (if any) will also not have its selection value - * modified as a result of calling `setGroup`. If you want to set the - * selection value of the new group, call `set` explicitly. - * - * @param {string} group - The name of the Crosstalk group, or null (or - * undefined) to clear the group. - */ - - - _createClass(SelectionHandle, [{ - key: "setGroup", - value: function setGroup(group) { - var _this = this; - - // If group is unchanged, do nothing - if (this._group === group) return; - // Treat null, undefined, and other falsy values the same - if (!this._group && !group) return; - - if (this._var) { - this._var.off("change", this._varOnChangeSub); - this._var = null; - this._varOnChangeSub = null; - } - - this._group = group; - - if (group) { - this._var = (0, _group2.default)(group).var("selection"); - var sub = this._var.on("change", function (e) { - _this._eventRelay.trigger("change", e, _this); - }); - this._varOnChangeSub = sub; - } - } - - /** - * Retrieves the current selection for the group represented by this - * `SelectionHandle`. - * - * - If no selection is active, then this value will be falsy. - * - If a selection is active, but no data points are selected, then this - * value will be an empty array. - * - If a selection is active, and data points are selected, then the keys - * of the selected data points will be present in the array. - */ - - }, { - key: "_mergeExtraInfo", - - - /** - * Combines the given `extraInfo` (if any) with the handle's default - * `_extraInfo` (if any). - * @private - */ - value: function _mergeExtraInfo(extraInfo) { - // Important incidental effect: shallow clone is returned - return util.extend({}, this._extraInfo ? this._extraInfo : null, extraInfo ? extraInfo : null); - } - - /** - * Overwrites the current selection for the group, and raises the `"change"` - * event among all of the group's '`SelectionHandle` instances (including - * this one). - * - * @fires SelectionHandle#change - * @param {string[]} selectedKeys - Falsy, empty array, or array of keys (see - * {@link SelectionHandle#value}). - * @param {Object} [extraInfo] - Extra properties to be included on the event - * object that's passed to listeners (in addition to any options that were - * passed into the `SelectionHandle` constructor). - */ - - }, { - key: "set", - value: function set(selectedKeys, extraInfo) { - if (this._var) this._var.set(selectedKeys, this._mergeExtraInfo(extraInfo)); - } - - /** - * Overwrites the current selection for the group, and raises the `"change"` - * event among all of the group's '`SelectionHandle` instances (including - * this one). - * - * @fires SelectionHandle#change - * @param {Object} [extraInfo] - Extra properties to be included on the event - * object that's passed to listeners (in addition to any that were passed - * into the `SelectionHandle` constructor). - */ - - }, { - key: "clear", - value: function clear(extraInfo) { - if (this._var) this.set(void 0, this._mergeExtraInfo(extraInfo)); - } - - /** - * Subscribes to events on this `SelectionHandle`. - * - * @param {string} eventType - Indicates the type of events to listen to. - * Currently, only `"change"` is supported. - * @param {SelectionHandle~listener} listener - The callback function that - * will be invoked when the event occurs. - * @return {string} - A token to pass to {@link SelectionHandle#off} to cancel - * this subscription. - */ - - }, { - key: "on", - value: function on(eventType, listener) { - return this._emitter.on(eventType, listener); - } - - /** - * Cancels event subscriptions created by {@link SelectionHandle#on}. - * - * @param {string} eventType - The type of event to unsubscribe. - * @param {string|SelectionHandle~listener} listener - Either the callback - * function previously passed into {@link SelectionHandle#on}, or the - * string that was returned from {@link SelectionHandle#on}. - */ - - }, { - key: "off", - value: function off(eventType, listener) { - return this._emitter.off(eventType, listener); - } - - /** - * Shuts down the `SelectionHandle` object. - * - * Removes all event listeners that were added through this handle. - */ - - }, { - key: "close", - value: function close() { - this._emitter.removeAllListeners(); - this.setGroup(null); - } - }, { - key: "value", - get: function get() { - return this._var ? this._var.get() : null; - } - }]); - - return SelectionHandle; -}(); - -/** - * @callback SelectionHandle~listener - * @param {Object} event - An object containing details of the event. For - * `"change"` events, this includes the properties `value` (the new - * value of the selection, or `undefined` if no selection is active), - * `oldValue` (the previous value of the selection), and `sender` (the - * `SelectionHandle` instance that made the change). - */ - -/** - * @event SelectionHandle#change - * @type {object} - * @property {object} value - The new value of the selection, or `undefined` - * if no selection is active. - * @property {object} oldValue - The previous value of the selection. - * @property {SelectionHandle} sender - The `SelectionHandle` instance that - * changed the value. - */ - -},{"./events":1,"./group":4,"./util":11}],11:[function(require,module,exports){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -exports.extend = extend; -exports.checkSorted = checkSorted; -exports.diffSortedLists = diffSortedLists; -exports.dataframeToD3 = dataframeToD3; - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function extend(target) { - for (var _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - sources[_key - 1] = arguments[_key]; - } - - for (var i = 0; i < sources.length; i++) { - var src = sources[i]; - if (typeof src === "undefined" || src === null) continue; - - for (var key in src) { - if (src.hasOwnProperty(key)) { - target[key] = src[key]; - } - } - } - return target; -} - -function checkSorted(list) { - for (var i = 1; i < list.length; i++) { - if (list[i] <= list[i - 1]) { - throw new Error("List is not sorted or contains duplicate"); - } - } -} - -function diffSortedLists(a, b) { - var i_a = 0; - var i_b = 0; - - if (!a) a = []; - if (!b) b = []; - - var a_only = []; - var b_only = []; - - checkSorted(a); - checkSorted(b); - - while (i_a < a.length && i_b < b.length) { - if (a[i_a] === b[i_b]) { - i_a++; - i_b++; - } else if (a[i_a] < b[i_b]) { - a_only.push(a[i_a++]); - } else { - b_only.push(b[i_b++]); - } - } - - if (i_a < a.length) a_only = a_only.concat(a.slice(i_a)); - if (i_b < b.length) b_only = b_only.concat(b.slice(i_b)); - return { - removed: a_only, - added: b_only - }; -} - -// Convert from wide: { colA: [1,2,3], colB: [4,5,6], ... } -// to long: [ {colA: 1, colB: 4}, {colA: 2, colB: 5}, ... ] -function dataframeToD3(df) { - var names = []; - var length = void 0; - for (var name in df) { - if (df.hasOwnProperty(name)) names.push(name); - if (_typeof(df[name]) !== "object" || typeof df[name].length === "undefined") { - throw new Error("All fields must be arrays"); - } else if (typeof length !== "undefined" && length !== df[name].length) { - throw new Error("All fields must be arrays of the same length"); - } - length = df[name].length; - } - var results = []; - var item = void 0; - for (var row = 0; row < length; row++) { - item = {}; - for (var col = 0; col < names.length; col++) { - item[names[col]] = df[names[col]][row]; - } - results.push(item); - } - return results; -} - -/** - * Keeps track of all event listener additions/removals and lets all active - * listeners be removed with a single operation. - * - * @private - */ - -var SubscriptionTracker = exports.SubscriptionTracker = function () { - function SubscriptionTracker(emitter) { - _classCallCheck(this, SubscriptionTracker); - - this._emitter = emitter; - this._subs = {}; - } - - _createClass(SubscriptionTracker, [{ - key: "on", - value: function on(eventType, listener) { - var sub = this._emitter.on(eventType, listener); - this._subs[sub] = eventType; - return sub; - } - }, { - key: "off", - value: function off(eventType, listener) { - var sub = this._emitter.off(eventType, listener); - if (sub) { - delete this._subs[sub]; - } - return sub; - } - }, { - key: "removeAllListeners", - value: function removeAllListeners() { - var _this = this; - - var current_subs = this._subs; - this._subs = {}; - Object.keys(current_subs).forEach(function (sub) { - _this._emitter.off(current_subs[sub], sub); - }); - } - }]); - - return SubscriptionTracker; -}(); - -},{}],12:[function(require,module,exports){ -(function (global){ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _events = require("./events"); - -var _events2 = _interopRequireDefault(_events); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var Var = function () { - function Var(group, name, /*optional*/value) { - _classCallCheck(this, Var); - - this._group = group; - this._name = name; - this._value = value; - this._events = new _events2.default(); - } - - _createClass(Var, [{ - key: "get", - value: function get() { - return this._value; - } - }, { - key: "set", - value: function set(value, /*optional*/event) { - if (this._value === value) { - // Do nothing; the value hasn't changed - return; - } - var oldValue = this._value; - this._value = value; - // Alert JavaScript listeners that the value has changed - var evt = {}; - if (event && (typeof event === "undefined" ? "undefined" : _typeof(event)) === "object") { - for (var k in event) { - if (event.hasOwnProperty(k)) evt[k] = event[k]; - } - } - evt.oldValue = oldValue; - evt.value = value; - this._events.trigger("change", evt, this); - - // TODO: Make this extensible, to let arbitrary back-ends know that - // something has changed - if (global.Shiny && global.Shiny.onInputChange) { - global.Shiny.onInputChange(".clientValue-" + (this._group.name !== null ? this._group.name + "-" : "") + this._name, typeof value === "undefined" ? null : value); - } - } - }, { - key: "on", - value: function on(eventType, listener) { - return this._events.on(eventType, listener); - } - }, { - key: "off", - value: function off(eventType, listener) { - return this._events.off(eventType, listener); - } - }]); - - return Var; -}(); - -exports.default = Var; - -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) - -},{"./events":1}]},{},[5]) -//# sourceMappingURL=crosstalk.js.map diff --git a/site_libs/crosstalk-1.2.1/js/crosstalk.js.map b/site_libs/crosstalk-1.2.1/js/crosstalk.js.map deleted file mode 100644 index cff94f08..00000000 --- a/site_libs/crosstalk-1.2.1/js/crosstalk.js.map +++ /dev/null @@ -1,37 +0,0 @@ -{ - "version": 3, - "sources": [ - "node_modules/browser-pack/_prelude.js", - "javascript/src/events.js", - "javascript/src/filter.js", - "javascript/src/filterset.js", - "javascript/src/group.js", - "javascript/src/index.js", - "javascript/src/input.js", - "javascript/src/input_checkboxgroup.js", - "javascript/src/input_selectize.js", - "javascript/src/input_slider.js", - "javascript/src/selection.js", - "javascript/src/util.js", - "javascript/src/var.js" - ], - "names": [], - "mappings": "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", - "file": "generated.js", - "sourceRoot": "", - "sourcesContent": [ - "(function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o {\n this._eventRelay.trigger(\"change\", e, this);\n });\n this._varOnChangeSub = sub;\n }\n }\n\n /**\n * Combine the given `extraInfo` (if any) with the handle's default\n * `_extraInfo` (if any).\n * @private\n */\n _mergeExtraInfo(extraInfo) {\n return util.extend({},\n this._extraInfo ? this._extraInfo : null,\n extraInfo ? extraInfo : null);\n }\n\n /**\n * Close the handle. This clears this handle's contribution to the filter set,\n * and unsubscribes all event listeners.\n */\n close() {\n this._emitter.removeAllListeners();\n this.clear();\n this.setGroup(null);\n }\n\n /**\n * Clear this handle's contribution to the filter set.\n *\n * @param {Object} [extraInfo] - Extra properties to be included on the event\n * object that's passed to listeners (in addition to any options that were\n * passed into the `FilterHandle` constructor).\n * \n * @fires FilterHandle#change\n */\n clear(extraInfo) {\n if (!this._filterSet)\n return;\n this._filterSet.clear(this._id);\n this._onChange(extraInfo);\n }\n\n /**\n * Set this handle's contribution to the filter set. This array should consist\n * of the keys of the rows that _should_ be displayed; any keys that are not\n * present in the array will be considered _filtered out_. Note that multiple\n * `FilterHandle` instances in the group may each contribute an array of keys,\n * and only those keys that appear in _all_ of the arrays make it through the\n * filter.\n *\n * @param {string[]} keys - Empty array, or array of keys. To clear the\n * filter, don't pass an empty array; instead, use the\n * {@link FilterHandle#clear} method.\n * @param {Object} [extraInfo] - Extra properties to be included on the event\n * object that's passed to listeners (in addition to any options that were\n * passed into the `FilterHandle` constructor).\n * \n * @fires FilterHandle#change\n */\n set(keys, extraInfo) {\n if (!this._filterSet)\n return;\n this._filterSet.update(this._id, keys);\n this._onChange(extraInfo);\n }\n\n /**\n * @return {string[]|null} - Either: 1) an array of keys that made it through\n * all of the `FilterHandle` instances, or, 2) `null`, which means no filter\n * is being applied (all data should be displayed).\n */\n get filteredKeys() {\n return this._filterSet ? this._filterSet.value : null;\n }\n\n /**\n * Subscribe to events on this `FilterHandle`.\n *\n * @param {string} eventType - Indicates the type of events to listen to.\n * Currently, only `\"change\"` is supported.\n * @param {FilterHandle~listener} listener - The callback function that\n * will be invoked when the event occurs.\n * @return {string} - A token to pass to {@link FilterHandle#off} to cancel\n * this subscription.\n */\n on(eventType, listener) {\n return this._emitter.on(eventType, listener);\n }\n\n /**\n * Cancel event subscriptions created by {@link FilterHandle#on}.\n *\n * @param {string} eventType - The type of event to unsubscribe.\n * @param {string|FilterHandle~listener} listener - Either the callback\n * function previously passed into {@link FilterHandle#on}, or the\n * string that was returned from {@link FilterHandle#on}.\n */\n off(eventType, listener) {\n return this._emitter.off(eventType, listener);\n }\n\n _onChange(extraInfo) {\n if (!this._filterSet)\n return;\n this._filterVar.set(this._filterSet.value, this._mergeExtraInfo(extraInfo));\n }\n\n /**\n * @callback FilterHandle~listener\n * @param {Object} event - An object containing details of the event. For\n * `\"change\"` events, this includes the properties `value` (the new\n * value of the filter set, or `null` if no filter set is active),\n * `oldValue` (the previous value of the filter set), and `sender` (the\n * `FilterHandle` instance that made the change).\n */\n\n}\n\n/**\n * @event FilterHandle#change\n * @type {object}\n * @property {object} value - The new value of the filter set, or `null`\n * if no filter set is active.\n * @property {object} oldValue - The previous value of the filter set.\n * @property {FilterHandle} sender - The `FilterHandle` instance that\n * changed the value.\n */\n", - "import { diffSortedLists } from \"./util\";\n\nfunction naturalComparator(a, b) {\n if (a === b) {\n return 0;\n } else if (a < b) {\n return -1;\n } else if (a > b) {\n return 1;\n }\n}\n\n/**\n * @private\n */\nexport default class FilterSet {\n constructor() {\n this.reset();\n }\n\n reset() {\n // Key: handle ID, Value: array of selected keys, or null\n this._handles = {};\n // Key: key string, Value: count of handles that include it\n this._keys = {};\n this._value = null;\n this._activeHandles = 0;\n }\n\n get value() {\n return this._value;\n }\n\n update(handleId, keys) {\n if (keys !== null) {\n keys = keys.slice(0); // clone before sorting\n keys.sort(naturalComparator);\n }\n\n let {added, removed} = diffSortedLists(this._handles[handleId], keys);\n this._handles[handleId] = keys;\n\n for (let i = 0; i < added.length; i++) {\n this._keys[added[i]] = (this._keys[added[i]] || 0) + 1;\n }\n for (let i = 0; i < removed.length; i++) {\n this._keys[removed[i]]--;\n }\n\n this._updateValue(keys);\n }\n\n /**\n * @param {string[]} keys Sorted array of strings that indicate\n * a superset of possible keys.\n * @private\n */\n _updateValue(keys = this._allKeys) {\n let handleCount = Object.keys(this._handles).length;\n if (handleCount === 0) {\n this._value = null;\n } else {\n this._value = [];\n for (let i = 0; i < keys.length; i++) {\n let count = this._keys[keys[i]];\n if (count === handleCount) {\n this._value.push(keys[i]);\n }\n }\n }\n }\n\n clear(handleId) {\n if (typeof(this._handles[handleId]) === \"undefined\") {\n return;\n }\n\n let keys = this._handles[handleId];\n if (!keys) {\n keys = [];\n }\n\n for (let i = 0; i < keys.length; i++) {\n this._keys[keys[i]]--;\n }\n delete this._handles[handleId];\n\n this._updateValue();\n }\n\n get _allKeys() {\n let allKeys = Object.keys(this._keys);\n allKeys.sort(naturalComparator);\n return allKeys;\n }\n}\n", - "import Var from \"./var\";\n\n// Use a global so that multiple copies of crosstalk.js can be loaded and still\n// have groups behave as singletons across all copies.\nglobal.__crosstalk_groups = global.__crosstalk_groups || {};\nlet groups = global.__crosstalk_groups;\n\nexport default function group(groupName) {\n if (groupName && typeof(groupName) === \"string\") {\n if (!groups.hasOwnProperty(groupName)) {\n groups[groupName] = new Group(groupName);\n }\n return groups[groupName];\n } else if (typeof(groupName) === \"object\" && groupName._vars && groupName.var) {\n // Appears to already be a group object\n return groupName;\n } else if (Array.isArray(groupName) &&\n groupName.length == 1 &&\n typeof(groupName[0]) === \"string\") {\n return group(groupName[0]);\n } else {\n throw new Error(\"Invalid groupName argument\");\n }\n}\n\nclass Group {\n constructor(name) {\n this.name = name;\n this._vars = {};\n }\n\n var(name) {\n if (!name || typeof(name) !== \"string\") {\n throw new Error(\"Invalid var name\");\n }\n\n if (!this._vars.hasOwnProperty(name))\n this._vars[name] = new Var(this, name);\n return this._vars[name];\n }\n\n has(name) {\n if (!name || typeof(name) !== \"string\") {\n throw new Error(\"Invalid var name\");\n }\n\n return this._vars.hasOwnProperty(name);\n }\n}\n", - "import group from \"./group\";\nimport { SelectionHandle } from \"./selection\";\nimport { FilterHandle } from \"./filter\";\nimport { bind } from \"./input\";\nimport \"./input_selectize\";\nimport \"./input_checkboxgroup\";\nimport \"./input_slider\";\n\nconst defaultGroup = group(\"default\");\n\nfunction var_(name) {\n return defaultGroup.var(name);\n}\n\nfunction has(name) {\n return defaultGroup.has(name);\n}\n\nif (global.Shiny) {\n global.Shiny.addCustomMessageHandler(\"update-client-value\", function(message) {\n if (typeof(message.group) === \"string\") {\n group(message.group).var(message.name).set(message.value);\n } else {\n var_(message.name).set(message.value);\n }\n });\n}\n\nconst crosstalk = {\n group: group,\n var: var_,\n has: has,\n SelectionHandle: SelectionHandle,\n FilterHandle: FilterHandle,\n bind: bind\n};\n\n/**\n * @namespace crosstalk\n */\nexport default crosstalk;\nglobal.crosstalk = crosstalk;\n", - "let $ = global.jQuery;\n\nlet bindings = {};\n\nexport function register(reg) {\n bindings[reg.className] = reg;\n if (global.document && global.document.readyState !== \"complete\") {\n $(() => {\n bind();\n });\n } else if (global.document) {\n setTimeout(bind, 100);\n }\n}\n\nexport function bind() {\n Object.keys(bindings).forEach(function(className) {\n let binding = bindings[className];\n $(\".\" + binding.className).not(\".crosstalk-input-bound\").each(function(i, el) {\n bindInstance(binding, el);\n });\n });\n}\n\n// Escape jQuery identifier\nfunction $escape(val) {\n return val.replace(/([!\"#$%&'()*+,./:;<=>?@[\\\\\\]^`{|}~])/g, \"\\\\$1\");\n}\n\nfunction bindEl(el) {\n let $el = $(el);\n Object.keys(bindings).forEach(function(className) {\n if ($el.hasClass(className) && !$el.hasClass(\"crosstalk-input-bound\")) {\n let binding = bindings[className];\n bindInstance(binding, el);\n }\n });\n}\n\nfunction bindInstance(binding, el) {\n let jsonEl = $(el).find(\"script[type='application/json'][data-for='\" + $escape(el.id) + \"']\");\n let data = JSON.parse(jsonEl[0].innerText);\n\n let instance = binding.factory(el, data);\n $(el).data(\"crosstalk-instance\", instance);\n $(el).addClass(\"crosstalk-input-bound\");\n}\n\nif (global.Shiny) {\n let inputBinding = new global.Shiny.InputBinding();\n let $ = global.jQuery;\n $.extend(inputBinding, {\n find: function(scope) {\n return $(scope).find(\".crosstalk-input\");\n },\n initialize: function(el) {\n if (!$(el).hasClass(\"crosstalk-input-bound\")) {\n bindEl(el);\n }\n },\n getId: function(el) {\n return el.id;\n },\n getValue: function(el) {\n\n },\n setValue: function(el, value) {\n\n },\n receiveMessage: function(el, data) {\n\n },\n subscribe: function(el, callback) {\n $(el).data(\"crosstalk-instance\").resume();\n },\n unsubscribe: function(el) {\n $(el).data(\"crosstalk-instance\").suspend();\n }\n });\n global.Shiny.inputBindings.register(inputBinding, \"crosstalk.inputBinding\");\n}\n", - "import * as input from \"./input\";\nimport { FilterHandle } from \"./filter\";\n\nlet $ = global.jQuery;\n\ninput.register({\n className: \"crosstalk-input-checkboxgroup\",\n\n factory: function(el, data) {\n /*\n * map: {\"groupA\": [\"keyA\", \"keyB\", ...], ...}\n * group: \"ct-groupname\"\n */\n let ctHandle = new FilterHandle(data.group);\n\n let lastKnownKeys;\n let $el = $(el);\n $el.on(\"change\", \"input[type='checkbox']\", function() {\n let checked = $el.find(\"input[type='checkbox']:checked\");\n if (checked.length === 0) {\n lastKnownKeys = null;\n ctHandle.clear();\n } else {\n let keys = {};\n checked.each(function() {\n data.map[this.value].forEach(function(key) {\n keys[key] = true;\n });\n });\n let keyArray = Object.keys(keys);\n keyArray.sort();\n lastKnownKeys = keyArray;\n ctHandle.set(keyArray);\n }\n });\n\n return {\n suspend: function() {\n ctHandle.clear();\n },\n resume: function() {\n if (lastKnownKeys)\n ctHandle.set(lastKnownKeys);\n }\n };\n }\n});\n", - "import * as input from \"./input\";\nimport * as util from \"./util\";\nimport { FilterHandle } from \"./filter\";\n\nlet $ = global.jQuery;\n\ninput.register({\n className: \"crosstalk-input-select\",\n\n factory: function(el, data) {\n /*\n * items: {value: [...], label: [...]}\n * map: {\"groupA\": [\"keyA\", \"keyB\", ...], ...}\n * group: \"ct-groupname\"\n */\n\n let first = [{value: \"\", label: \"(All)\"}];\n let items = util.dataframeToD3(data.items);\n let opts = {\n options: first.concat(items),\n valueField: \"value\",\n labelField: \"label\",\n searchField: \"label\"\n };\n\n let select = $(el).find(\"select\")[0];\n\n let selectize = $(select).selectize(opts)[0].selectize;\n\n let ctHandle = new FilterHandle(data.group);\n\n let lastKnownKeys;\n selectize.on(\"change\", function() {\n if (selectize.items.length === 0) {\n lastKnownKeys = null;\n ctHandle.clear();\n } else {\n let keys = {};\n selectize.items.forEach(function(group) {\n data.map[group].forEach(function(key) {\n keys[key] = true;\n });\n });\n let keyArray = Object.keys(keys);\n keyArray.sort();\n lastKnownKeys = keyArray;\n ctHandle.set(keyArray);\n }\n });\n\n return {\n suspend: function() {\n ctHandle.clear();\n },\n resume: function() {\n if (lastKnownKeys)\n ctHandle.set(lastKnownKeys);\n }\n };\n }\n});\n", - "import * as input from \"./input\";\nimport { FilterHandle } from \"./filter\";\n\nlet $ = global.jQuery;\nlet strftime = global.strftime;\n\ninput.register({\n className: \"crosstalk-input-slider\",\n\n factory: function(el, data) {\n /*\n * map: {\"groupA\": [\"keyA\", \"keyB\", ...], ...}\n * group: \"ct-groupname\"\n */\n let ctHandle = new FilterHandle(data.group);\n\n let opts = {};\n let $el = $(el).find(\"input\");\n let dataType = $el.data(\"data-type\");\n let timeFormat = $el.data(\"time-format\");\n let round = $el.data(\"round\");\n let timeFormatter;\n\n // Set up formatting functions\n if (dataType === \"date\") {\n timeFormatter = strftime.utc();\n opts.prettify = function(num) {\n return timeFormatter(timeFormat, new Date(num));\n };\n\n } else if (dataType === \"datetime\") {\n let timezone = $el.data(\"timezone\");\n if (timezone)\n timeFormatter = strftime.timezone(timezone);\n else\n timeFormatter = strftime;\n\n opts.prettify = function(num) {\n return timeFormatter(timeFormat, new Date(num));\n };\n } else if (dataType === \"number\") {\n if (typeof round !== \"undefined\")\n opts.prettify = function(num) {\n let factor = Math.pow(10, round);\n return Math.round(num * factor) / factor;\n };\n }\n\n $el.ionRangeSlider(opts);\n\n function getValue() {\n let result = $el.data(\"ionRangeSlider\").result;\n\n // Function for converting numeric value from slider to appropriate type.\n let convert;\n let dataType = $el.data(\"data-type\");\n if (dataType === \"date\") {\n convert = function(val) {\n return formatDateUTC(new Date(+val));\n };\n } else if (dataType === \"datetime\") {\n convert = function(val) {\n // Convert ms to s\n return +val / 1000;\n };\n } else {\n convert = function(val) { return +val; };\n }\n\n if ($el.data(\"ionRangeSlider\").options.type === \"double\") {\n return [convert(result.from), convert(result.to)];\n } else {\n return convert(result.from);\n }\n }\n\n let lastKnownKeys = null;\n\n $el.on(\"change.crosstalkSliderInput\", function(event) {\n if (!$el.data(\"updating\") && !$el.data(\"animating\")) {\n let [from, to] = getValue();\n let keys = [];\n for (let i = 0; i < data.values.length; i++) {\n let val = data.values[i];\n if (val >= from && val <= to) {\n keys.push(data.keys[i]);\n }\n }\n keys.sort();\n ctHandle.set(keys);\n lastKnownKeys = keys;\n }\n });\n\n\n // let $el = $(el);\n // $el.on(\"change\", \"input[type=\"checkbox\"]\", function() {\n // let checked = $el.find(\"input[type=\"checkbox\"]:checked\");\n // if (checked.length === 0) {\n // ctHandle.clear();\n // } else {\n // let keys = {};\n // checked.each(function() {\n // data.map[this.value].forEach(function(key) {\n // keys[key] = true;\n // });\n // });\n // let keyArray = Object.keys(keys);\n // keyArray.sort();\n // ctHandle.set(keyArray);\n // }\n // });\n\n return {\n suspend: function() {\n ctHandle.clear();\n },\n resume: function() {\n if (lastKnownKeys)\n ctHandle.set(lastKnownKeys);\n }\n };\n }\n});\n\n\n// Convert a number to a string with leading zeros\nfunction padZeros(n, digits) {\n let str = n.toString();\n while (str.length < digits)\n str = \"0\" + str;\n return str;\n}\n\n// Given a Date object, return a string in yyyy-mm-dd format, using the\n// UTC date. This may be a day off from the date in the local time zone.\nfunction formatDateUTC(date) {\n if (date instanceof Date) {\n return date.getUTCFullYear() + \"-\" +\n padZeros(date.getUTCMonth()+1, 2) + \"-\" +\n padZeros(date.getUTCDate(), 2);\n\n } else {\n return null;\n }\n}\n", - "import Events from \"./events\";\nimport grp from \"./group\";\nimport * as util from \"./util\";\n\n/**\n * Use this class to read and write (and listen for changes to) the selection\n * for a Crosstalk group. This is intended to be used for linked brushing.\n *\n * If two (or more) `SelectionHandle` instances in the same webpage share the\n * same group name, they will share the same state. Setting the selection using\n * one `SelectionHandle` instance will result in the `value` property instantly\n * changing across the others, and `\"change\"` event listeners on all instances\n * (including the one that initiated the sending) will fire.\n *\n * @param {string} [group] - The name of the Crosstalk group, or if none,\n * null or undefined (or any other falsy value). This can be changed later\n * via the [SelectionHandle#setGroup](#setGroup) method.\n * @param {Object} [extraInfo] - An object whose properties will be copied to\n * the event object whenever an event is emitted.\n */\nexport class SelectionHandle {\n\n constructor(group = null, extraInfo = null) {\n this._eventRelay = new Events();\n this._emitter = new util.SubscriptionTracker(this._eventRelay);\n\n // Name of the group we're currently tracking, if any. Can change over time.\n this._group = null;\n // The Var we're currently tracking, if any. Can change over time.\n this._var = null;\n // The event handler subscription we currently have on var.on(\"change\").\n this._varOnChangeSub = null;\n\n this._extraInfo = util.extend({ sender: this }, extraInfo);\n\n this.setGroup(group);\n }\n\n /**\n * Changes the Crosstalk group membership of this SelectionHandle. The group\n * being switched away from (if any) will not have its selection value\n * modified as a result of calling `setGroup`, even if this handle was the\n * most recent handle to set the selection of the group.\n *\n * The group being switched to (if any) will also not have its selection value\n * modified as a result of calling `setGroup`. If you want to set the\n * selection value of the new group, call `set` explicitly.\n *\n * @param {string} group - The name of the Crosstalk group, or null (or\n * undefined) to clear the group.\n */\n setGroup(group) {\n // If group is unchanged, do nothing\n if (this._group === group)\n return;\n // Treat null, undefined, and other falsy values the same\n if (!this._group && !group)\n return;\n\n if (this._var) {\n this._var.off(\"change\", this._varOnChangeSub);\n this._var = null;\n this._varOnChangeSub = null;\n }\n\n this._group = group;\n\n if (group) {\n this._var = grp(group).var(\"selection\");\n let sub = this._var.on(\"change\", (e) => {\n this._eventRelay.trigger(\"change\", e, this);\n });\n this._varOnChangeSub = sub;\n }\n }\n\n /**\n * Retrieves the current selection for the group represented by this\n * `SelectionHandle`.\n *\n * - If no selection is active, then this value will be falsy.\n * - If a selection is active, but no data points are selected, then this\n * value will be an empty array.\n * - If a selection is active, and data points are selected, then the keys\n * of the selected data points will be present in the array.\n */\n get value() {\n return this._var ? this._var.get() : null;\n }\n\n /**\n * Combines the given `extraInfo` (if any) with the handle's default\n * `_extraInfo` (if any).\n * @private\n */\n _mergeExtraInfo(extraInfo) {\n // Important incidental effect: shallow clone is returned\n return util.extend({},\n this._extraInfo ? this._extraInfo : null,\n extraInfo ? extraInfo : null);\n }\n\n /**\n * Overwrites the current selection for the group, and raises the `\"change\"`\n * event among all of the group's '`SelectionHandle` instances (including\n * this one).\n *\n * @fires SelectionHandle#change\n * @param {string[]} selectedKeys - Falsy, empty array, or array of keys (see\n * {@link SelectionHandle#value}).\n * @param {Object} [extraInfo] - Extra properties to be included on the event\n * object that's passed to listeners (in addition to any options that were\n * passed into the `SelectionHandle` constructor).\n */\n set(selectedKeys, extraInfo) {\n if (this._var)\n this._var.set(selectedKeys, this._mergeExtraInfo(extraInfo));\n }\n\n /**\n * Overwrites the current selection for the group, and raises the `\"change\"`\n * event among all of the group's '`SelectionHandle` instances (including\n * this one).\n *\n * @fires SelectionHandle#change\n * @param {Object} [extraInfo] - Extra properties to be included on the event\n * object that's passed to listeners (in addition to any that were passed\n * into the `SelectionHandle` constructor).\n */\n clear(extraInfo) {\n if (this._var)\n this.set(void 0, this._mergeExtraInfo(extraInfo));\n }\n\n /**\n * Subscribes to events on this `SelectionHandle`.\n *\n * @param {string} eventType - Indicates the type of events to listen to.\n * Currently, only `\"change\"` is supported.\n * @param {SelectionHandle~listener} listener - The callback function that\n * will be invoked when the event occurs.\n * @return {string} - A token to pass to {@link SelectionHandle#off} to cancel\n * this subscription.\n */\n on(eventType, listener) {\n return this._emitter.on(eventType, listener);\n }\n\n /**\n * Cancels event subscriptions created by {@link SelectionHandle#on}.\n *\n * @param {string} eventType - The type of event to unsubscribe.\n * @param {string|SelectionHandle~listener} listener - Either the callback\n * function previously passed into {@link SelectionHandle#on}, or the\n * string that was returned from {@link SelectionHandle#on}.\n */\n off(eventType, listener) {\n return this._emitter.off(eventType, listener);\n }\n\n /**\n * Shuts down the `SelectionHandle` object.\n *\n * Removes all event listeners that were added through this handle.\n */\n close() {\n this._emitter.removeAllListeners();\n this.setGroup(null);\n }\n}\n\n/**\n * @callback SelectionHandle~listener\n * @param {Object} event - An object containing details of the event. For\n * `\"change\"` events, this includes the properties `value` (the new\n * value of the selection, or `undefined` if no selection is active),\n * `oldValue` (the previous value of the selection), and `sender` (the\n * `SelectionHandle` instance that made the change).\n */\n\n/**\n * @event SelectionHandle#change\n * @type {object}\n * @property {object} value - The new value of the selection, or `undefined`\n * if no selection is active.\n * @property {object} oldValue - The previous value of the selection.\n * @property {SelectionHandle} sender - The `SelectionHandle` instance that\n * changed the value.\n */\n", - "export function extend(target, ...sources) {\n for (let i = 0; i < sources.length; i++) {\n let src = sources[i];\n if (typeof(src) === \"undefined\" || src === null)\n continue;\n\n for (let key in src) {\n if (src.hasOwnProperty(key)) {\n target[key] = src[key];\n }\n }\n }\n return target;\n}\n\nexport function checkSorted(list) {\n for (let i = 1; i < list.length; i++) {\n if (list[i] <= list[i-1]) {\n throw new Error(\"List is not sorted or contains duplicate\");\n }\n }\n}\n\nexport function diffSortedLists(a, b) {\n let i_a = 0;\n let i_b = 0;\n\n if (!a) a = [];\n if (!b) b = [];\n\n let a_only = [];\n let b_only = [];\n\n checkSorted(a);\n checkSorted(b);\n\n while (i_a < a.length && i_b < b.length) {\n if (a[i_a] === b[i_b]) {\n i_a++;\n i_b++;\n } else if (a[i_a] < b[i_b]) {\n a_only.push(a[i_a++]);\n } else {\n b_only.push(b[i_b++]);\n }\n }\n\n if (i_a < a.length)\n a_only = a_only.concat(a.slice(i_a));\n if (i_b < b.length)\n b_only = b_only.concat(b.slice(i_b));\n return {\n removed: a_only,\n added: b_only\n };\n}\n\n// Convert from wide: { colA: [1,2,3], colB: [4,5,6], ... }\n// to long: [ {colA: 1, colB: 4}, {colA: 2, colB: 5}, ... ]\nexport function dataframeToD3(df) {\n let names = [];\n let length;\n for (let name in df) {\n if (df.hasOwnProperty(name))\n names.push(name);\n if (typeof(df[name]) !== \"object\" || typeof(df[name].length) === \"undefined\") {\n throw new Error(\"All fields must be arrays\");\n } else if (typeof(length) !== \"undefined\" && length !== df[name].length) {\n throw new Error(\"All fields must be arrays of the same length\");\n }\n length = df[name].length;\n }\n let results = [];\n let item;\n for (let row = 0; row < length; row++) {\n item = {};\n for (let col = 0; col < names.length; col++) {\n item[names[col]] = df[names[col]][row];\n }\n results.push(item);\n }\n return results;\n}\n\n/**\n * Keeps track of all event listener additions/removals and lets all active\n * listeners be removed with a single operation.\n *\n * @private\n */\nexport class SubscriptionTracker {\n constructor(emitter) {\n this._emitter = emitter;\n this._subs = {};\n }\n\n on(eventType, listener) {\n let sub = this._emitter.on(eventType, listener);\n this._subs[sub] = eventType;\n return sub;\n }\n\n off(eventType, listener) {\n let sub = this._emitter.off(eventType, listener);\n if (sub) {\n delete this._subs[sub];\n }\n return sub;\n }\n\n removeAllListeners() {\n let current_subs = this._subs;\n this._subs = {};\n Object.keys(current_subs).forEach((sub) => {\n this._emitter.off(current_subs[sub], sub);\n });\n }\n}\n", - "import Events from \"./events\";\n\nexport default class Var {\n constructor(group, name, /*optional*/ value) {\n this._group = group;\n this._name = name;\n this._value = value;\n this._events = new Events();\n }\n\n get() {\n return this._value;\n }\n\n set(value, /*optional*/ event) {\n if (this._value === value) {\n // Do nothing; the value hasn't changed\n return;\n }\n let oldValue = this._value;\n this._value = value;\n // Alert JavaScript listeners that the value has changed\n let evt = {};\n if (event && typeof(event) === \"object\") {\n for (let k in event) {\n if (event.hasOwnProperty(k))\n evt[k] = event[k];\n }\n }\n evt.oldValue = oldValue;\n evt.value = value;\n this._events.trigger(\"change\", evt, this);\n\n // TODO: Make this extensible, to let arbitrary back-ends know that\n // something has changed\n if (global.Shiny && global.Shiny.onInputChange) {\n global.Shiny.onInputChange(\n \".clientValue-\" +\n (this._group.name !== null ? this._group.name + \"-\" : \"\") +\n this._name,\n typeof(value) === \"undefined\" ? null : value\n );\n }\n }\n\n on(eventType, listener) {\n return this._events.on(eventType, listener);\n }\n\n off(eventType, listener) {\n return this._events.off(eventType, listener);\n }\n}\n" - ] -} \ No newline at end of file diff --git a/site_libs/crosstalk-1.2.1/js/crosstalk.min.js b/site_libs/crosstalk-1.2.1/js/crosstalk.min.js deleted file mode 100644 index b7ec0ac9..00000000 --- a/site_libs/crosstalk-1.2.1/js/crosstalk.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function o(u,a,l){function s(n,e){if(!a[n]){if(!u[n]){var t="function"==typeof require&&require;if(!e&&t)return t(n,!0);if(f)return f(n,!0);var r=new Error("Cannot find module '"+n+"'");throw r.code="MODULE_NOT_FOUND",r}var i=a[n]={exports:{}};u[n][0].call(i.exports,function(e){var t=u[n][1][e];return s(t||e)},i,i.exports,o,u,a,l)}return a[n].exports}for(var f="function"==typeof require&&require,e=0;e?@[\\\]^`{|}~])/g,"\\$1")+"']"),r=JSON.parse(n[0].innerText),i=e.factory(t,r);o(t).data("crosstalk-instance",i),o(t).addClass("crosstalk-input-bound")}if(t.Shiny){var e=new t.Shiny.InputBinding,u=t.jQuery;u.extend(e,{find:function(e){return u(e).find(".crosstalk-input")},initialize:function(e){var t,n;u(e).hasClass("crosstalk-input-bound")||(n=o(t=e),Object.keys(r).forEach(function(e){n.hasClass(e)&&!n.hasClass("crosstalk-input-bound")&&i(r[e],t)}))},getId:function(e){return e.id},getValue:function(e){},setValue:function(e,t){},receiveMessage:function(e,t){},subscribe:function(e,t){u(e).data("crosstalk-instance").resume()},unsubscribe:function(e){u(e).data("crosstalk-instance").suspend()}}),t.Shiny.inputBindings.register(e,"crosstalk.inputBinding")}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],7:[function(r,e,t){(function(e){"use strict";var t=function(e){{if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}}(r("./input")),n=r("./filter");var a=e.jQuery;t.register({className:"crosstalk-input-checkboxgroup",factory:function(e,r){var i=new n.FilterHandle(r.group),o=void 0,u=a(e);return u.on("change","input[type='checkbox']",function(){var e=u.find("input[type='checkbox']:checked");if(0===e.length)o=null,i.clear();else{var t={};e.each(function(){r.map[this.value].forEach(function(e){t[e]=!0})});var n=Object.keys(t);n.sort(),o=n,i.set(n)}}),{suspend:function(){i.clear()},resume:function(){o&&i.set(o)}}}})}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./filter":2,"./input":6}],8:[function(r,e,t){(function(e){"use strict";var t=n(r("./input")),l=n(r("./util")),s=r("./filter");function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}var f=e.jQuery;t.register({className:"crosstalk-input-select",factory:function(e,n){var t=l.dataframeToD3(n.items),r={options:[{value:"",label:"(All)"}].concat(t),valueField:"value",labelField:"label",searchField:"label"},i=f(e).find("select")[0],o=f(i).selectize(r)[0].selectize,u=new s.FilterHandle(n.group),a=void 0;return o.on("change",function(){if(0===o.items.length)a=null,u.clear();else{var t={};o.items.forEach(function(e){n.map[e].forEach(function(e){t[e]=!0})});var e=Object.keys(t);e.sort(),a=e,u.set(e)}}),{suspend:function(){u.clear()},resume:function(){a&&u.set(a)}}}})}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./filter":2,"./input":6,"./util":11}],9:[function(n,e,t){(function(e){"use strict";var d=function(e,t){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return function(e,t){var n=[],r=!0,i=!1,o=void 0;try{for(var u,a=e[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!t||n.length!==t);r=!0);}catch(e){i=!0,o=e}finally{try{!r&&a.return&&a.return()}finally{if(i)throw o}}return n}(e,t);throw new TypeError("Invalid attempt to destructure non-iterable instance")},t=function(e){{if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}}(n("./input")),a=n("./filter");var v=e.jQuery,p=e.strftime;function y(e,t){for(var n=e.toString();n.length {\n this._eventRelay.trigger(\"change\", e, this);\n });\n this._varOnChangeSub = sub;\n }\n }\n\n /**\n * Combine the given `extraInfo` (if any) with the handle's default\n * `_extraInfo` (if any).\n * @private\n */\n _mergeExtraInfo(extraInfo) {\n return util.extend({},\n this._extraInfo ? this._extraInfo : null,\n extraInfo ? extraInfo : null);\n }\n\n /**\n * Close the handle. This clears this handle's contribution to the filter set,\n * and unsubscribes all event listeners.\n */\n close() {\n this._emitter.removeAllListeners();\n this.clear();\n this.setGroup(null);\n }\n\n /**\n * Clear this handle's contribution to the filter set.\n *\n * @param {Object} [extraInfo] - Extra properties to be included on the event\n * object that's passed to listeners (in addition to any options that were\n * passed into the `FilterHandle` constructor).\n * \n * @fires FilterHandle#change\n */\n clear(extraInfo) {\n if (!this._filterSet)\n return;\n this._filterSet.clear(this._id);\n this._onChange(extraInfo);\n }\n\n /**\n * Set this handle's contribution to the filter set. This array should consist\n * of the keys of the rows that _should_ be displayed; any keys that are not\n * present in the array will be considered _filtered out_. Note that multiple\n * `FilterHandle` instances in the group may each contribute an array of keys,\n * and only those keys that appear in _all_ of the arrays make it through the\n * filter.\n *\n * @param {string[]} keys - Empty array, or array of keys. To clear the\n * filter, don't pass an empty array; instead, use the\n * {@link FilterHandle#clear} method.\n * @param {Object} [extraInfo] - Extra properties to be included on the event\n * object that's passed to listeners (in addition to any options that were\n * passed into the `FilterHandle` constructor).\n * \n * @fires FilterHandle#change\n */\n set(keys, extraInfo) {\n if (!this._filterSet)\n return;\n this._filterSet.update(this._id, keys);\n this._onChange(extraInfo);\n }\n\n /**\n * @return {string[]|null} - Either: 1) an array of keys that made it through\n * all of the `FilterHandle` instances, or, 2) `null`, which means no filter\n * is being applied (all data should be displayed).\n */\n get filteredKeys() {\n return this._filterSet ? this._filterSet.value : null;\n }\n\n /**\n * Subscribe to events on this `FilterHandle`.\n *\n * @param {string} eventType - Indicates the type of events to listen to.\n * Currently, only `\"change\"` is supported.\n * @param {FilterHandle~listener} listener - The callback function that\n * will be invoked when the event occurs.\n * @return {string} - A token to pass to {@link FilterHandle#off} to cancel\n * this subscription.\n */\n on(eventType, listener) {\n return this._emitter.on(eventType, listener);\n }\n\n /**\n * Cancel event subscriptions created by {@link FilterHandle#on}.\n *\n * @param {string} eventType - The type of event to unsubscribe.\n * @param {string|FilterHandle~listener} listener - Either the callback\n * function previously passed into {@link FilterHandle#on}, or the\n * string that was returned from {@link FilterHandle#on}.\n */\n off(eventType, listener) {\n return this._emitter.off(eventType, listener);\n }\n\n _onChange(extraInfo) {\n if (!this._filterSet)\n return;\n this._filterVar.set(this._filterSet.value, this._mergeExtraInfo(extraInfo));\n }\n\n /**\n * @callback FilterHandle~listener\n * @param {Object} event - An object containing details of the event. For\n * `\"change\"` events, this includes the properties `value` (the new\n * value of the filter set, or `null` if no filter set is active),\n * `oldValue` (the previous value of the filter set), and `sender` (the\n * `FilterHandle` instance that made the change).\n */\n\n}\n\n/**\n * @event FilterHandle#change\n * @type {object}\n * @property {object} value - The new value of the filter set, or `null`\n * if no filter set is active.\n * @property {object} oldValue - The previous value of the filter set.\n * @property {FilterHandle} sender - The `FilterHandle` instance that\n * changed the value.\n */\n","import { diffSortedLists } from \"./util\";\n\nfunction naturalComparator(a, b) {\n if (a === b) {\n return 0;\n } else if (a < b) {\n return -1;\n } else if (a > b) {\n return 1;\n }\n}\n\n/**\n * @private\n */\nexport default class FilterSet {\n constructor() {\n this.reset();\n }\n\n reset() {\n // Key: handle ID, Value: array of selected keys, or null\n this._handles = {};\n // Key: key string, Value: count of handles that include it\n this._keys = {};\n this._value = null;\n this._activeHandles = 0;\n }\n\n get value() {\n return this._value;\n }\n\n update(handleId, keys) {\n if (keys !== null) {\n keys = keys.slice(0); // clone before sorting\n keys.sort(naturalComparator);\n }\n\n let {added, removed} = diffSortedLists(this._handles[handleId], keys);\n this._handles[handleId] = keys;\n\n for (let i = 0; i < added.length; i++) {\n this._keys[added[i]] = (this._keys[added[i]] || 0) + 1;\n }\n for (let i = 0; i < removed.length; i++) {\n this._keys[removed[i]]--;\n }\n\n this._updateValue(keys);\n }\n\n /**\n * @param {string[]} keys Sorted array of strings that indicate\n * a superset of possible keys.\n * @private\n */\n _updateValue(keys = this._allKeys) {\n let handleCount = Object.keys(this._handles).length;\n if (handleCount === 0) {\n this._value = null;\n } else {\n this._value = [];\n for (let i = 0; i < keys.length; i++) {\n let count = this._keys[keys[i]];\n if (count === handleCount) {\n this._value.push(keys[i]);\n }\n }\n }\n }\n\n clear(handleId) {\n if (typeof(this._handles[handleId]) === \"undefined\") {\n return;\n }\n\n let keys = this._handles[handleId];\n if (!keys) {\n keys = [];\n }\n\n for (let i = 0; i < keys.length; i++) {\n this._keys[keys[i]]--;\n }\n delete this._handles[handleId];\n\n this._updateValue();\n }\n\n get _allKeys() {\n let allKeys = Object.keys(this._keys);\n allKeys.sort(naturalComparator);\n return allKeys;\n }\n}\n","import Var from \"./var\";\n\n// Use a global so that multiple copies of crosstalk.js can be loaded and still\n// have groups behave as singletons across all copies.\nglobal.__crosstalk_groups = global.__crosstalk_groups || {};\nlet groups = global.__crosstalk_groups;\n\nexport default function group(groupName) {\n if (groupName && typeof(groupName) === \"string\") {\n if (!groups.hasOwnProperty(groupName)) {\n groups[groupName] = new Group(groupName);\n }\n return groups[groupName];\n } else if (typeof(groupName) === \"object\" && groupName._vars && groupName.var) {\n // Appears to already be a group object\n return groupName;\n } else if (Array.isArray(groupName) &&\n groupName.length == 1 &&\n typeof(groupName[0]) === \"string\") {\n return group(groupName[0]);\n } else {\n throw new Error(\"Invalid groupName argument\");\n }\n}\n\nclass Group {\n constructor(name) {\n this.name = name;\n this._vars = {};\n }\n\n var(name) {\n if (!name || typeof(name) !== \"string\") {\n throw new Error(\"Invalid var name\");\n }\n\n if (!this._vars.hasOwnProperty(name))\n this._vars[name] = new Var(this, name);\n return this._vars[name];\n }\n\n has(name) {\n if (!name || typeof(name) !== \"string\") {\n throw new Error(\"Invalid var name\");\n }\n\n return this._vars.hasOwnProperty(name);\n }\n}\n","import group from \"./group\";\nimport { SelectionHandle } from \"./selection\";\nimport { FilterHandle } from \"./filter\";\nimport { bind } from \"./input\";\nimport \"./input_selectize\";\nimport \"./input_checkboxgroup\";\nimport \"./input_slider\";\n\nconst defaultGroup = group(\"default\");\n\nfunction var_(name) {\n return defaultGroup.var(name);\n}\n\nfunction has(name) {\n return defaultGroup.has(name);\n}\n\nif (global.Shiny) {\n global.Shiny.addCustomMessageHandler(\"update-client-value\", function(message) {\n if (typeof(message.group) === \"string\") {\n group(message.group).var(message.name).set(message.value);\n } else {\n var_(message.name).set(message.value);\n }\n });\n}\n\nconst crosstalk = {\n group: group,\n var: var_,\n has: has,\n SelectionHandle: SelectionHandle,\n FilterHandle: FilterHandle,\n bind: bind\n};\n\n/**\n * @namespace crosstalk\n */\nexport default crosstalk;\nglobal.crosstalk = crosstalk;\n","let $ = global.jQuery;\n\nlet bindings = {};\n\nexport function register(reg) {\n bindings[reg.className] = reg;\n if (global.document && global.document.readyState !== \"complete\") {\n $(() => {\n bind();\n });\n } else if (global.document) {\n setTimeout(bind, 100);\n }\n}\n\nexport function bind() {\n Object.keys(bindings).forEach(function(className) {\n let binding = bindings[className];\n $(\".\" + binding.className).not(\".crosstalk-input-bound\").each(function(i, el) {\n bindInstance(binding, el);\n });\n });\n}\n\n// Escape jQuery identifier\nfunction $escape(val) {\n return val.replace(/([!\"#$%&'()*+,./:;<=>?@[\\\\\\]^`{|}~])/g, \"\\\\$1\");\n}\n\nfunction bindEl(el) {\n let $el = $(el);\n Object.keys(bindings).forEach(function(className) {\n if ($el.hasClass(className) && !$el.hasClass(\"crosstalk-input-bound\")) {\n let binding = bindings[className];\n bindInstance(binding, el);\n }\n });\n}\n\nfunction bindInstance(binding, el) {\n let jsonEl = $(el).find(\"script[type='application/json'][data-for='\" + $escape(el.id) + \"']\");\n let data = JSON.parse(jsonEl[0].innerText);\n\n let instance = binding.factory(el, data);\n $(el).data(\"crosstalk-instance\", instance);\n $(el).addClass(\"crosstalk-input-bound\");\n}\n\nif (global.Shiny) {\n let inputBinding = new global.Shiny.InputBinding();\n let $ = global.jQuery;\n $.extend(inputBinding, {\n find: function(scope) {\n return $(scope).find(\".crosstalk-input\");\n },\n initialize: function(el) {\n if (!$(el).hasClass(\"crosstalk-input-bound\")) {\n bindEl(el);\n }\n },\n getId: function(el) {\n return el.id;\n },\n getValue: function(el) {\n\n },\n setValue: function(el, value) {\n\n },\n receiveMessage: function(el, data) {\n\n },\n subscribe: function(el, callback) {\n $(el).data(\"crosstalk-instance\").resume();\n },\n unsubscribe: function(el) {\n $(el).data(\"crosstalk-instance\").suspend();\n }\n });\n global.Shiny.inputBindings.register(inputBinding, \"crosstalk.inputBinding\");\n}\n","import * as input from \"./input\";\nimport { FilterHandle } from \"./filter\";\n\nlet $ = global.jQuery;\n\ninput.register({\n className: \"crosstalk-input-checkboxgroup\",\n\n factory: function(el, data) {\n /*\n * map: {\"groupA\": [\"keyA\", \"keyB\", ...], ...}\n * group: \"ct-groupname\"\n */\n let ctHandle = new FilterHandle(data.group);\n\n let lastKnownKeys;\n let $el = $(el);\n $el.on(\"change\", \"input[type='checkbox']\", function() {\n let checked = $el.find(\"input[type='checkbox']:checked\");\n if (checked.length === 0) {\n lastKnownKeys = null;\n ctHandle.clear();\n } else {\n let keys = {};\n checked.each(function() {\n data.map[this.value].forEach(function(key) {\n keys[key] = true;\n });\n });\n let keyArray = Object.keys(keys);\n keyArray.sort();\n lastKnownKeys = keyArray;\n ctHandle.set(keyArray);\n }\n });\n\n return {\n suspend: function() {\n ctHandle.clear();\n },\n resume: function() {\n if (lastKnownKeys)\n ctHandle.set(lastKnownKeys);\n }\n };\n }\n});\n","import * as input from \"./input\";\nimport * as util from \"./util\";\nimport { FilterHandle } from \"./filter\";\n\nlet $ = global.jQuery;\n\ninput.register({\n className: \"crosstalk-input-select\",\n\n factory: function(el, data) {\n /*\n * items: {value: [...], label: [...]}\n * map: {\"groupA\": [\"keyA\", \"keyB\", ...], ...}\n * group: \"ct-groupname\"\n */\n\n let first = [{value: \"\", label: \"(All)\"}];\n let items = util.dataframeToD3(data.items);\n let opts = {\n options: first.concat(items),\n valueField: \"value\",\n labelField: \"label\",\n searchField: \"label\"\n };\n\n let select = $(el).find(\"select\")[0];\n\n let selectize = $(select).selectize(opts)[0].selectize;\n\n let ctHandle = new FilterHandle(data.group);\n\n let lastKnownKeys;\n selectize.on(\"change\", function() {\n if (selectize.items.length === 0) {\n lastKnownKeys = null;\n ctHandle.clear();\n } else {\n let keys = {};\n selectize.items.forEach(function(group) {\n data.map[group].forEach(function(key) {\n keys[key] = true;\n });\n });\n let keyArray = Object.keys(keys);\n keyArray.sort();\n lastKnownKeys = keyArray;\n ctHandle.set(keyArray);\n }\n });\n\n return {\n suspend: function() {\n ctHandle.clear();\n },\n resume: function() {\n if (lastKnownKeys)\n ctHandle.set(lastKnownKeys);\n }\n };\n }\n});\n","import * as input from \"./input\";\nimport { FilterHandle } from \"./filter\";\n\nlet $ = global.jQuery;\nlet strftime = global.strftime;\n\ninput.register({\n className: \"crosstalk-input-slider\",\n\n factory: function(el, data) {\n /*\n * map: {\"groupA\": [\"keyA\", \"keyB\", ...], ...}\n * group: \"ct-groupname\"\n */\n let ctHandle = new FilterHandle(data.group);\n\n let opts = {};\n let $el = $(el).find(\"input\");\n let dataType = $el.data(\"data-type\");\n let timeFormat = $el.data(\"time-format\");\n let round = $el.data(\"round\");\n let timeFormatter;\n\n // Set up formatting functions\n if (dataType === \"date\") {\n timeFormatter = strftime.utc();\n opts.prettify = function(num) {\n return timeFormatter(timeFormat, new Date(num));\n };\n\n } else if (dataType === \"datetime\") {\n let timezone = $el.data(\"timezone\");\n if (timezone)\n timeFormatter = strftime.timezone(timezone);\n else\n timeFormatter = strftime;\n\n opts.prettify = function(num) {\n return timeFormatter(timeFormat, new Date(num));\n };\n } else if (dataType === \"number\") {\n if (typeof round !== \"undefined\")\n opts.prettify = function(num) {\n let factor = Math.pow(10, round);\n return Math.round(num * factor) / factor;\n };\n }\n\n $el.ionRangeSlider(opts);\n\n function getValue() {\n let result = $el.data(\"ionRangeSlider\").result;\n\n // Function for converting numeric value from slider to appropriate type.\n let convert;\n let dataType = $el.data(\"data-type\");\n if (dataType === \"date\") {\n convert = function(val) {\n return formatDateUTC(new Date(+val));\n };\n } else if (dataType === \"datetime\") {\n convert = function(val) {\n // Convert ms to s\n return +val / 1000;\n };\n } else {\n convert = function(val) { return +val; };\n }\n\n if ($el.data(\"ionRangeSlider\").options.type === \"double\") {\n return [convert(result.from), convert(result.to)];\n } else {\n return convert(result.from);\n }\n }\n\n let lastKnownKeys = null;\n\n $el.on(\"change.crosstalkSliderInput\", function(event) {\n if (!$el.data(\"updating\") && !$el.data(\"animating\")) {\n let [from, to] = getValue();\n let keys = [];\n for (let i = 0; i < data.values.length; i++) {\n let val = data.values[i];\n if (val >= from && val <= to) {\n keys.push(data.keys[i]);\n }\n }\n keys.sort();\n ctHandle.set(keys);\n lastKnownKeys = keys;\n }\n });\n\n\n // let $el = $(el);\n // $el.on(\"change\", \"input[type=\"checkbox\"]\", function() {\n // let checked = $el.find(\"input[type=\"checkbox\"]:checked\");\n // if (checked.length === 0) {\n // ctHandle.clear();\n // } else {\n // let keys = {};\n // checked.each(function() {\n // data.map[this.value].forEach(function(key) {\n // keys[key] = true;\n // });\n // });\n // let keyArray = Object.keys(keys);\n // keyArray.sort();\n // ctHandle.set(keyArray);\n // }\n // });\n\n return {\n suspend: function() {\n ctHandle.clear();\n },\n resume: function() {\n if (lastKnownKeys)\n ctHandle.set(lastKnownKeys);\n }\n };\n }\n});\n\n\n// Convert a number to a string with leading zeros\nfunction padZeros(n, digits) {\n let str = n.toString();\n while (str.length < digits)\n str = \"0\" + str;\n return str;\n}\n\n// Given a Date object, return a string in yyyy-mm-dd format, using the\n// UTC date. This may be a day off from the date in the local time zone.\nfunction formatDateUTC(date) {\n if (date instanceof Date) {\n return date.getUTCFullYear() + \"-\" +\n padZeros(date.getUTCMonth()+1, 2) + \"-\" +\n padZeros(date.getUTCDate(), 2);\n\n } else {\n return null;\n }\n}\n","import Events from \"./events\";\nimport grp from \"./group\";\nimport * as util from \"./util\";\n\n/**\n * Use this class to read and write (and listen for changes to) the selection\n * for a Crosstalk group. This is intended to be used for linked brushing.\n *\n * If two (or more) `SelectionHandle` instances in the same webpage share the\n * same group name, they will share the same state. Setting the selection using\n * one `SelectionHandle` instance will result in the `value` property instantly\n * changing across the others, and `\"change\"` event listeners on all instances\n * (including the one that initiated the sending) will fire.\n *\n * @param {string} [group] - The name of the Crosstalk group, or if none,\n * null or undefined (or any other falsy value). This can be changed later\n * via the [SelectionHandle#setGroup](#setGroup) method.\n * @param {Object} [extraInfo] - An object whose properties will be copied to\n * the event object whenever an event is emitted.\n */\nexport class SelectionHandle {\n\n constructor(group = null, extraInfo = null) {\n this._eventRelay = new Events();\n this._emitter = new util.SubscriptionTracker(this._eventRelay);\n\n // Name of the group we're currently tracking, if any. Can change over time.\n this._group = null;\n // The Var we're currently tracking, if any. Can change over time.\n this._var = null;\n // The event handler subscription we currently have on var.on(\"change\").\n this._varOnChangeSub = null;\n\n this._extraInfo = util.extend({ sender: this }, extraInfo);\n\n this.setGroup(group);\n }\n\n /**\n * Changes the Crosstalk group membership of this SelectionHandle. The group\n * being switched away from (if any) will not have its selection value\n * modified as a result of calling `setGroup`, even if this handle was the\n * most recent handle to set the selection of the group.\n *\n * The group being switched to (if any) will also not have its selection value\n * modified as a result of calling `setGroup`. If you want to set the\n * selection value of the new group, call `set` explicitly.\n *\n * @param {string} group - The name of the Crosstalk group, or null (or\n * undefined) to clear the group.\n */\n setGroup(group) {\n // If group is unchanged, do nothing\n if (this._group === group)\n return;\n // Treat null, undefined, and other falsy values the same\n if (!this._group && !group)\n return;\n\n if (this._var) {\n this._var.off(\"change\", this._varOnChangeSub);\n this._var = null;\n this._varOnChangeSub = null;\n }\n\n this._group = group;\n\n if (group) {\n this._var = grp(group).var(\"selection\");\n let sub = this._var.on(\"change\", (e) => {\n this._eventRelay.trigger(\"change\", e, this);\n });\n this._varOnChangeSub = sub;\n }\n }\n\n /**\n * Retrieves the current selection for the group represented by this\n * `SelectionHandle`.\n *\n * - If no selection is active, then this value will be falsy.\n * - If a selection is active, but no data points are selected, then this\n * value will be an empty array.\n * - If a selection is active, and data points are selected, then the keys\n * of the selected data points will be present in the array.\n */\n get value() {\n return this._var ? this._var.get() : null;\n }\n\n /**\n * Combines the given `extraInfo` (if any) with the handle's default\n * `_extraInfo` (if any).\n * @private\n */\n _mergeExtraInfo(extraInfo) {\n // Important incidental effect: shallow clone is returned\n return util.extend({},\n this._extraInfo ? this._extraInfo : null,\n extraInfo ? extraInfo : null);\n }\n\n /**\n * Overwrites the current selection for the group, and raises the `\"change\"`\n * event among all of the group's '`SelectionHandle` instances (including\n * this one).\n *\n * @fires SelectionHandle#change\n * @param {string[]} selectedKeys - Falsy, empty array, or array of keys (see\n * {@link SelectionHandle#value}).\n * @param {Object} [extraInfo] - Extra properties to be included on the event\n * object that's passed to listeners (in addition to any options that were\n * passed into the `SelectionHandle` constructor).\n */\n set(selectedKeys, extraInfo) {\n if (this._var)\n this._var.set(selectedKeys, this._mergeExtraInfo(extraInfo));\n }\n\n /**\n * Overwrites the current selection for the group, and raises the `\"change\"`\n * event among all of the group's '`SelectionHandle` instances (including\n * this one).\n *\n * @fires SelectionHandle#change\n * @param {Object} [extraInfo] - Extra properties to be included on the event\n * object that's passed to listeners (in addition to any that were passed\n * into the `SelectionHandle` constructor).\n */\n clear(extraInfo) {\n if (this._var)\n this.set(void 0, this._mergeExtraInfo(extraInfo));\n }\n\n /**\n * Subscribes to events on this `SelectionHandle`.\n *\n * @param {string} eventType - Indicates the type of events to listen to.\n * Currently, only `\"change\"` is supported.\n * @param {SelectionHandle~listener} listener - The callback function that\n * will be invoked when the event occurs.\n * @return {string} - A token to pass to {@link SelectionHandle#off} to cancel\n * this subscription.\n */\n on(eventType, listener) {\n return this._emitter.on(eventType, listener);\n }\n\n /**\n * Cancels event subscriptions created by {@link SelectionHandle#on}.\n *\n * @param {string} eventType - The type of event to unsubscribe.\n * @param {string|SelectionHandle~listener} listener - Either the callback\n * function previously passed into {@link SelectionHandle#on}, or the\n * string that was returned from {@link SelectionHandle#on}.\n */\n off(eventType, listener) {\n return this._emitter.off(eventType, listener);\n }\n\n /**\n * Shuts down the `SelectionHandle` object.\n *\n * Removes all event listeners that were added through this handle.\n */\n close() {\n this._emitter.removeAllListeners();\n this.setGroup(null);\n }\n}\n\n/**\n * @callback SelectionHandle~listener\n * @param {Object} event - An object containing details of the event. For\n * `\"change\"` events, this includes the properties `value` (the new\n * value of the selection, or `undefined` if no selection is active),\n * `oldValue` (the previous value of the selection), and `sender` (the\n * `SelectionHandle` instance that made the change).\n */\n\n/**\n * @event SelectionHandle#change\n * @type {object}\n * @property {object} value - The new value of the selection, or `undefined`\n * if no selection is active.\n * @property {object} oldValue - The previous value of the selection.\n * @property {SelectionHandle} sender - The `SelectionHandle` instance that\n * changed the value.\n */\n","export function extend(target, ...sources) {\n for (let i = 0; i < sources.length; i++) {\n let src = sources[i];\n if (typeof(src) === \"undefined\" || src === null)\n continue;\n\n for (let key in src) {\n if (src.hasOwnProperty(key)) {\n target[key] = src[key];\n }\n }\n }\n return target;\n}\n\nexport function checkSorted(list) {\n for (let i = 1; i < list.length; i++) {\n if (list[i] <= list[i-1]) {\n throw new Error(\"List is not sorted or contains duplicate\");\n }\n }\n}\n\nexport function diffSortedLists(a, b) {\n let i_a = 0;\n let i_b = 0;\n\n if (!a) a = [];\n if (!b) b = [];\n\n let a_only = [];\n let b_only = [];\n\n checkSorted(a);\n checkSorted(b);\n\n while (i_a < a.length && i_b < b.length) {\n if (a[i_a] === b[i_b]) {\n i_a++;\n i_b++;\n } else if (a[i_a] < b[i_b]) {\n a_only.push(a[i_a++]);\n } else {\n b_only.push(b[i_b++]);\n }\n }\n\n if (i_a < a.length)\n a_only = a_only.concat(a.slice(i_a));\n if (i_b < b.length)\n b_only = b_only.concat(b.slice(i_b));\n return {\n removed: a_only,\n added: b_only\n };\n}\n\n// Convert from wide: { colA: [1,2,3], colB: [4,5,6], ... }\n// to long: [ {colA: 1, colB: 4}, {colA: 2, colB: 5}, ... ]\nexport function dataframeToD3(df) {\n let names = [];\n let length;\n for (let name in df) {\n if (df.hasOwnProperty(name))\n names.push(name);\n if (typeof(df[name]) !== \"object\" || typeof(df[name].length) === \"undefined\") {\n throw new Error(\"All fields must be arrays\");\n } else if (typeof(length) !== \"undefined\" && length !== df[name].length) {\n throw new Error(\"All fields must be arrays of the same length\");\n }\n length = df[name].length;\n }\n let results = [];\n let item;\n for (let row = 0; row < length; row++) {\n item = {};\n for (let col = 0; col < names.length; col++) {\n item[names[col]] = df[names[col]][row];\n }\n results.push(item);\n }\n return results;\n}\n\n/**\n * Keeps track of all event listener additions/removals and lets all active\n * listeners be removed with a single operation.\n *\n * @private\n */\nexport class SubscriptionTracker {\n constructor(emitter) {\n this._emitter = emitter;\n this._subs = {};\n }\n\n on(eventType, listener) {\n let sub = this._emitter.on(eventType, listener);\n this._subs[sub] = eventType;\n return sub;\n }\n\n off(eventType, listener) {\n let sub = this._emitter.off(eventType, listener);\n if (sub) {\n delete this._subs[sub];\n }\n return sub;\n }\n\n removeAllListeners() {\n let current_subs = this._subs;\n this._subs = {};\n Object.keys(current_subs).forEach((sub) => {\n this._emitter.off(current_subs[sub], sub);\n });\n }\n}\n","import Events from \"./events\";\n\nexport default class Var {\n constructor(group, name, /*optional*/ value) {\n this._group = group;\n this._name = name;\n this._value = value;\n this._events = new Events();\n }\n\n get() {\n return this._value;\n }\n\n set(value, /*optional*/ event) {\n if (this._value === value) {\n // Do nothing; the value hasn't changed\n return;\n }\n let oldValue = this._value;\n this._value = value;\n // Alert JavaScript listeners that the value has changed\n let evt = {};\n if (event && typeof(event) === \"object\") {\n for (let k in event) {\n if (event.hasOwnProperty(k))\n evt[k] = event[k];\n }\n }\n evt.oldValue = oldValue;\n evt.value = value;\n this._events.trigger(\"change\", evt, this);\n\n // TODO: Make this extensible, to let arbitrary back-ends know that\n // something has changed\n if (global.Shiny && global.Shiny.onInputChange) {\n global.Shiny.onInputChange(\n \".clientValue-\" +\n (this._group.name !== null ? this._group.name + \"-\" : \"\") +\n this._name,\n typeof(value) === \"undefined\" ? null : value\n );\n }\n }\n\n on(eventType, listener) {\n return this._events.on(eventType, listener);\n }\n\n off(eventType, listener) {\n return this._events.off(eventType, listener);\n }\n}\n"]} \ No newline at end of file diff --git a/site_libs/crosstalk-1.2.1/scss/crosstalk.scss b/site_libs/crosstalk-1.2.1/scss/crosstalk.scss deleted file mode 100644 index 35665616..00000000 --- a/site_libs/crosstalk-1.2.1/scss/crosstalk.scss +++ /dev/null @@ -1,75 +0,0 @@ -/* Adjust margins outwards, so column contents line up with the edges of the - parent of container-fluid. */ -.container-fluid.crosstalk-bscols { - margin-left: -30px; - margin-right: -30px; - white-space: normal; -} - -/* But don't adjust the margins outwards if we're directly under the body, - i.e. we were the top-level of something at the console. */ -body > .container-fluid.crosstalk-bscols { - margin-left: auto; - margin-right: auto; -} - -.crosstalk-input-checkboxgroup .crosstalk-options-group .crosstalk-options-column { - display: inline-block; - padding-right: 12px; - vertical-align: top; -} - -@media only screen and (max-width:480px) { - .crosstalk-input-checkboxgroup .crosstalk-options-group .crosstalk-options-column { - display: block; - padding-right: inherit; - } -} - -/* Relevant BS3 styles to make filter_checkbox() look reasonable without Bootstrap */ -.crosstalk-input { - margin-bottom: 15px; /* a la .form-group */ - .control-label { - margin-bottom: 0; - vertical-align: middle; - } - input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px; - line-height: normal; - } - .checkbox { - position: relative; - display: block; - margin-top: 10px; - margin-bottom: 10px; - } - .checkbox > label{ - padding-left: 20px; - margin-bottom: 0; - font-weight: 400; - cursor: pointer; - } - .checkbox input[type="checkbox"], - .checkbox-inline input[type="checkbox"] { - position: absolute; - margin-top: 2px; - margin-left: -20px; - } - .checkbox + .checkbox { - margin-top: -5px; - } - .checkbox-inline { - position: relative; - display: inline-block; - padding-left: 20px; - margin-bottom: 0; - font-weight: 400; - vertical-align: middle; - cursor: pointer; - } - .checkbox-inline + .checkbox-inline { - margin-top: 0; - margin-left: 10px; - } -} diff --git a/site_libs/datatables-binding-0.33/datatables.js b/site_libs/datatables-binding-0.33/datatables.js deleted file mode 100644 index 765b53cb..00000000 --- a/site_libs/datatables-binding-0.33/datatables.js +++ /dev/null @@ -1,1539 +0,0 @@ -(function() { - -// some helper functions: using a global object DTWidget so that it can be used -// in JS() code, e.g. datatable(options = list(foo = JS('code'))); unlike R's -// dynamic scoping, when 'code' is eval'ed, JavaScript does not know objects -// from the "parent frame", e.g. JS('DTWidget') will not work unless it was made -// a global object -var DTWidget = {}; - -// 123456666.7890 -> 123,456,666.7890 -var markInterval = function(d, digits, interval, mark, decMark, precision) { - x = precision ? d.toPrecision(digits) : d.toFixed(digits); - if (!/^-?[\d.]+$/.test(x)) return x; - var xv = x.split('.'); - if (xv.length > 2) return x; // should have at most one decimal point - xv[0] = xv[0].replace(new RegExp('\\B(?=(\\d{' + interval + '})+(?!\\d))', 'g'), mark); - return xv.join(decMark); -}; - -DTWidget.formatCurrency = function(data, currency, digits, interval, mark, decMark, before, zeroPrint) { - var d = parseFloat(data); - if (isNaN(d)) return ''; - if (zeroPrint !== null && d === 0.0) return zeroPrint; - var res = markInterval(d, digits, interval, mark, decMark); - res = before ? (/^-/.test(res) ? '-' + currency + res.replace(/^-/, '') : currency + res) : - res + currency; - return res; -}; - -DTWidget.formatString = function(data, prefix, suffix) { - var d = data; - if (d === null) return ''; - return prefix + d + suffix; -}; - -DTWidget.formatPercentage = function(data, digits, interval, mark, decMark, zeroPrint) { - var d = parseFloat(data); - if (isNaN(d)) return ''; - if (zeroPrint !== null && d === 0.0) return zeroPrint; - return markInterval(d * 100, digits, interval, mark, decMark) + '%'; -}; - -DTWidget.formatRound = function(data, digits, interval, mark, decMark, zeroPrint) { - var d = parseFloat(data); - if (isNaN(d)) return ''; - if (zeroPrint !== null && d === 0.0) return zeroPrint; - return markInterval(d, digits, interval, mark, decMark); -}; - -DTWidget.formatSignif = function(data, digits, interval, mark, decMark, zeroPrint) { - var d = parseFloat(data); - if (isNaN(d)) return ''; - if (zeroPrint !== null && d === 0.0) return zeroPrint; - return markInterval(d, digits, interval, mark, decMark, true); -}; - -DTWidget.formatDate = function(data, method, params) { - var d = data; - if (d === null) return ''; - // (new Date('2015-10-28')).toDateString() may return 2015-10-27 because the - // actual time created could be like 'Tue Oct 27 2015 19:00:00 GMT-0500 (CDT)', - // i.e. the date-only string is treated as UTC time instead of local time - if ((method === 'toDateString' || method === 'toLocaleDateString') && /^\d{4,}\D\d{2}\D\d{2}$/.test(d)) { - d = d.split(/\D/); - d = new Date(d[0], d[1] - 1, d[2]); - } else { - d = new Date(d); - } - return d[method].apply(d, params); -}; - -window.DTWidget = DTWidget; - -// A helper function to update the properties of existing filters -var setFilterProps = function(td, props) { - // Update enabled/disabled state - var $input = $(td).find('input').first(); - var searchable = $input.data('searchable'); - $input.prop('disabled', !searchable || props.disabled); - - // Based on the filter type, set its new values - var type = td.getAttribute('data-type'); - if (['factor', 'logical'].includes(type)) { - // Reformat the new dropdown options for use with selectize - var new_vals = props.params.options.map(function(item) { - return { text: item, value: item }; - }); - - // Find the selectize object - var dropdown = $(td).find('.selectized').eq(0)[0].selectize; - - // Note the current values - var old_vals = dropdown.getValue(); - - // Remove the existing values - dropdown.clearOptions(); - - // Add the new options - dropdown.addOption(new_vals); - - // Preserve the existing values - dropdown.setValue(old_vals); - - } else if (['number', 'integer', 'date', 'time'].includes(type)) { - // Apply internal scaling to new limits. Updating scale not yet implemented. - var slider = $(td).find('.noUi-target').eq(0); - var scale = Math.pow(10, Math.max(0, +slider.data('scale') || 0)); - var new_vals = [props.params.min * scale, props.params.max * scale]; - - // Note what the new limits will be just for this filter - var new_lims = new_vals.slice(); - - // Determine the current values and limits - var old_vals = slider.val().map(Number); - var old_lims = slider.noUiSlider('options').range; - old_lims = [old_lims.min, old_lims.max]; - - // Preserve the current values if filters have been applied; otherwise, apply no filtering - if (old_vals[0] != old_lims[0]) { - new_vals[0] = Math.max(old_vals[0], new_vals[0]); - } - - if (old_vals[1] != old_lims[1]) { - new_vals[1] = Math.min(old_vals[1], new_vals[1]); - } - - // Update the endpoints of the slider - slider.noUiSlider({ - start: new_vals, - range: {'min': new_lims[0], 'max': new_lims[1]} - }, true); - } -}; - -var transposeArray2D = function(a) { - return a.length === 0 ? a : HTMLWidgets.transposeArray2D(a); -}; - -var crosstalkPluginsInstalled = false; - -function maybeInstallCrosstalkPlugins() { - if (crosstalkPluginsInstalled) - return; - crosstalkPluginsInstalled = true; - - $.fn.dataTable.ext.afnFiltering.push( - function(oSettings, aData, iDataIndex) { - var ctfilter = oSettings.nTable.ctfilter; - if (ctfilter && !ctfilter[iDataIndex]) - return false; - - var ctselect = oSettings.nTable.ctselect; - if (ctselect && !ctselect[iDataIndex]) - return false; - - return true; - } - ); -} - -HTMLWidgets.widget({ - name: "datatables", - type: "output", - renderOnNullValue: true, - initialize: function(el, width, height) { - // in order that the type=number inputs return a number - $.valHooks.number = { - get: function(el) { - var value = parseFloat(el.value); - return isNaN(value) ? "" : value; - } - }; - $(el).html(' '); - return { - data: null, - ctfilterHandle: new crosstalk.FilterHandle(), - ctfilterSubscription: null, - ctselectHandle: new crosstalk.SelectionHandle(), - ctselectSubscription: null - }; - }, - renderValue: function(el, data, instance) { - if (el.offsetWidth === 0 || el.offsetHeight === 0) { - instance.data = data; - return; - } - instance.data = null; - var $el = $(el); - $el.empty(); - - if (data === null) { - $el.append(' '); - // clear previous Shiny inputs (if any) - for (var i in instance.clearInputs) instance.clearInputs[i](); - instance.clearInputs = {}; - return; - } - - var crosstalkOptions = data.crosstalkOptions; - if (!crosstalkOptions) crosstalkOptions = { - 'key': null, 'group': null - }; - if (crosstalkOptions.group) { - maybeInstallCrosstalkPlugins(); - instance.ctfilterHandle.setGroup(crosstalkOptions.group); - instance.ctselectHandle.setGroup(crosstalkOptions.group); - } - - // if we are in the viewer then we always want to fillContainer and - // and autoHideNavigation (unless the user has explicitly set these) - if (window.HTMLWidgets.viewerMode) { - if (!data.hasOwnProperty("fillContainer")) - data.fillContainer = true; - if (!data.hasOwnProperty("autoHideNavigation")) - data.autoHideNavigation = true; - } - - // propagate fillContainer to instance (so we have it in resize) - instance.fillContainer = data.fillContainer; - - var cells = data.data; - - if (cells instanceof Array) cells = transposeArray2D(cells); - - $el.append(data.container); - var $table = $el.find('table'); - if (data.class) $table.addClass(data.class); - if (data.caption) $table.prepend(data.caption); - - if (!data.selection) data.selection = { - mode: 'none', selected: null, target: 'row', selectable: null - }; - if (HTMLWidgets.shinyMode && data.selection.mode !== 'none' && - data.selection.target === 'row+column') { - if ($table.children('tfoot').length === 0) { - $table.append($('')); - $table.find('thead tr').clone().appendTo($table.find('tfoot')); - } - } - - // column filters - var filterRow; - switch (data.filter) { - case 'top': - $table.children('thead').append(data.filterHTML); - filterRow = $table.find('thead tr:last td'); - break; - case 'bottom': - if ($table.children('tfoot').length === 0) { - $table.append($('')); - } - $table.children('tfoot').prepend(data.filterHTML); - filterRow = $table.find('tfoot tr:first td'); - break; - } - - var options = { searchDelay: 1000 }; - if (cells !== null) $.extend(options, { - data: cells - }); - - // options for fillContainer - var bootstrapActive = typeof($.fn.popover) != 'undefined'; - if (instance.fillContainer) { - - // force scrollX/scrollY and turn off autoWidth - options.scrollX = true; - options.scrollY = "100px"; // can be any value, we'll adjust below - - // if we aren't paginating then move around the info/filter controls - // to save space at the bottom and rephrase the info callback - if (data.options.paging === false) { - - // we know how to do this cleanly for bootstrap, not so much - // for other themes/layouts - if (bootstrapActive) { - options.dom = "<'row'<'col-sm-4'i><'col-sm-8'f>>" + - "<'row'<'col-sm-12'tr>>"; - } - - options.fnInfoCallback = function(oSettings, iStart, iEnd, - iMax, iTotal, sPre) { - return Number(iTotal).toLocaleString() + " records"; - }; - } - } - - // auto hide navigation if requested - // Note, this only works on client-side processing mode as on server-side, - // cells (data.data) is null; In addition, we require the pageLength option - // being provided explicitly to enable this. Despite we may be able to deduce - // the default value of pageLength, it may complicate things so we'd rather - // put this responsiblity to users and warn them on the R side. - if (data.autoHideNavigation === true && data.options.paging !== false) { - // strip all nav if length >= cells - if ((cells instanceof Array) && data.options.pageLength >= cells.length) - options.dom = bootstrapActive ? "<'row'<'col-sm-12'tr>>" : "t"; - // alternatively lean things out for flexdashboard mobile portrait - else if (bootstrapActive && window.FlexDashboard && window.FlexDashboard.isMobilePhone()) - options.dom = "<'row'<'col-sm-12'f>>" + - "<'row'<'col-sm-12'tr>>" + - "<'row'<'col-sm-12'p>>"; - } - - $.extend(true, options, data.options || {}); - - var searchCols = options.searchCols; - if (searchCols) { - searchCols = searchCols.map(function(x) { - return x === null ? '' : x.search; - }); - // FIXME: this means I don't respect the escapeRegex setting - delete options.searchCols; - } - - // server-side processing? - var server = options.serverSide === true; - - // use the dataSrc function to pre-process JSON data returned from R - var DT_rows_all = [], DT_rows_current = []; - if (server && HTMLWidgets.shinyMode && typeof options.ajax === 'object' && - /^session\/[\da-z]+\/dataobj/.test(options.ajax.url) && !options.ajax.dataSrc) { - options.ajax.dataSrc = function(json) { - DT_rows_all = $.makeArray(json.DT_rows_all); - DT_rows_current = $.makeArray(json.DT_rows_current); - var data = json.data; - if (!colReorderEnabled()) return data; - var table = $table.DataTable(), order = table.colReorder.order(), flag = true, i, j, row; - for (i = 0; i < order.length; ++i) if (order[i] !== i) flag = false; - if (flag) return data; - for (i = 0; i < data.length; ++i) { - row = data[i].slice(); - for (j = 0; j < order.length; ++j) data[i][j] = row[order[j]]; - } - return data; - }; - } - - var thiz = this; - if (instance.fillContainer) $table.on('init.dt', function(e) { - thiz.fillAvailableHeight(el, $(el).innerHeight()); - }); - // If the page contains serveral datatables and one of which enables colReorder, - // the table.colReorder.order() function will exist but throws error when called. - // So it seems like the only way to know if colReorder is enabled or not is to - // check the options. - var colReorderEnabled = function() { return "colReorder" in options; }; - var table = $table.DataTable(options); - $el.data('datatable', table); - - if ('rowGroup' in options) { - // Maintain RowGroup dataSrc when columns are reordered (#1109) - table.on('column-reorder', function(e, settings, details) { - var oldDataSrc = table.rowGroup().dataSrc(); - var newDataSrc = details.mapping[oldDataSrc]; - table.rowGroup().dataSrc(newDataSrc); - }); - } - - // Unregister previous Crosstalk event subscriptions, if they exist - if (instance.ctfilterSubscription) { - instance.ctfilterHandle.off("change", instance.ctfilterSubscription); - instance.ctfilterSubscription = null; - } - if (instance.ctselectSubscription) { - instance.ctselectHandle.off("change", instance.ctselectSubscription); - instance.ctselectSubscription = null; - } - - if (!crosstalkOptions.group) { - $table[0].ctfilter = null; - $table[0].ctselect = null; - } else { - var key = crosstalkOptions.key; - function keysToMatches(keys) { - if (!keys) { - return null; - } else { - var selectedKeys = {}; - for (var i = 0; i < keys.length; i++) { - selectedKeys[keys[i]] = true; - } - var matches = {}; - for (var j = 0; j < key.length; j++) { - if (selectedKeys[key[j]]) - matches[j] = true; - } - return matches; - } - } - - function applyCrosstalkFilter(e) { - $table[0].ctfilter = keysToMatches(e.value); - table.draw(); - } - instance.ctfilterSubscription = instance.ctfilterHandle.on("change", applyCrosstalkFilter); - applyCrosstalkFilter({value: instance.ctfilterHandle.filteredKeys}); - - function applyCrosstalkSelection(e) { - if (e.sender !== instance.ctselectHandle) { - table - .rows('.' + selClass, {search: 'applied'}) - .nodes() - .to$() - .removeClass(selClass); - if (selectedRows) - changeInput('rows_selected', selectedRows(), void 0, true); - } - - if (e.sender !== instance.ctselectHandle && e.value && e.value.length) { - var matches = keysToMatches(e.value); - - // persistent selection with plotly (& leaflet) - var ctOpts = crosstalk.var("plotlyCrosstalkOpts").get() || {}; - if (ctOpts.persistent === true) { - var matches = $.extend(matches, $table[0].ctselect); - } - - $table[0].ctselect = matches; - table.draw(); - } else { - if ($table[0].ctselect) { - $table[0].ctselect = null; - table.draw(); - } - } - } - instance.ctselectSubscription = instance.ctselectHandle.on("change", applyCrosstalkSelection); - // TODO: This next line doesn't seem to work when renderDataTable is used - applyCrosstalkSelection({value: instance.ctselectHandle.value}); - } - - var inArray = function(val, array) { - return $.inArray(val, $.makeArray(array)) > -1; - }; - - // search the i-th column - var searchColumn = function(i, value) { - var regex = false, ci = true; - if (options.search) { - regex = options.search.regex, - ci = options.search.caseInsensitive !== false; - } - // need to transpose the column index when colReorder is enabled - if (table.colReorder) i = table.colReorder.transpose(i); - return table.column(i).search(value, regex, !regex, ci); - }; - - if (data.filter !== 'none') { - if (!data.hasOwnProperty('filterSettings')) data.filterSettings = {}; - - filterRow.each(function(i, td) { - - var $td = $(td), type = $td.data('type'), filter; - var $input = $td.children('div').first().children('input'); - var disabled = $input.prop('disabled'); - var searchable = table.settings()[0].aoColumns[i].bSearchable; - $input.prop('disabled', !searchable || disabled); - $input.data('searchable', searchable); // for updating later - $input.on('input blur', function() { - $input.next('span').toggle(Boolean($input.val())); - }); - // Bootstrap sets pointer-events to none and we won't be able to click - // the clear button - $input.next('span').css('pointer-events', 'auto').hide().click(function() { - $(this).hide().prev('input').val('').trigger('input').focus(); - }); - var searchCol; // search string for this column - if (searchCols && searchCols[i]) { - searchCol = searchCols[i]; - $input.val(searchCol).trigger('input'); - } - var $x = $td.children('div').last(); - - // remove the overflow: hidden attribute of the scrollHead - // (otherwise the scrolling table body obscures the filters) - // The workaround and the discussion from - // https://github.com/rstudio/DT/issues/554#issuecomment-518007347 - // Otherwise the filter selection will not be anchored to the values - // when the columns number is many and scrollX is enabled. - var scrollHead = $(el).find('.dataTables_scrollHead,.dataTables_scrollFoot'); - var cssOverflowHead = scrollHead.css('overflow'); - var scrollBody = $(el).find('.dataTables_scrollBody'); - var cssOverflowBody = scrollBody.css('overflow'); - var scrollTable = $(el).find('.dataTables_scroll'); - var cssOverflowTable = scrollTable.css('overflow'); - if (cssOverflowHead === 'hidden') { - $x.on('show hide', function(e) { - if (e.type === 'show') { - scrollHead.css('overflow', 'visible'); - scrollBody.css('overflow', 'visible'); - scrollTable.css('overflow-x', 'scroll'); - } else { - scrollHead.css('overflow', cssOverflowHead); - scrollBody.css('overflow', cssOverflowBody); - scrollTable.css('overflow-x', cssOverflowTable); - } - }); - $x.css('z-index', 25); - } - - if (inArray(type, ['factor', 'logical'])) { - $input.on({ - click: function() { - $input.parent().hide(); $x.show().trigger('show'); filter[0].selectize.focus(); - }, - input: function() { - var v1 = JSON.stringify(filter[0].selectize.getValue()), v2 = $input.val(); - if (v1 === '[]') v1 = ''; - if (v1 !== v2) filter[0].selectize.setValue(v2 === '' ? [] : JSON.parse(v2)); - } - }); - var $input2 = $x.children('select'); - filter = $input2.selectize($.extend({ - options: $input2.data('options').map(function(v, i) { - return ({text: v, value: v}); - }), - plugins: ['remove_button'], - hideSelected: true, - onChange: function(value) { - if (value === null) value = []; // compatibility with jQuery 3.0 - $input.val(value.length ? JSON.stringify(value) : ''); - if (value.length) $input.trigger('input'); - $input.attr('title', $input.val()); - if (server) { - searchColumn(i, value.length ? JSON.stringify(value) : '').draw(); - return; - } - // turn off filter if nothing selected - $td.data('filter', value.length > 0); - table.draw(); // redraw table, and filters will be applied - } - }, data.filterSettings.select)); - filter[0].selectize.on('blur', function() { - $x.hide().trigger('hide'); $input.parent().show(); $input.trigger('blur'); - }); - filter.next('div').css('margin-bottom', 'auto'); - } else if (type === 'character') { - var fun = function() { - searchColumn(i, $input.val()).draw(); - }; - // throttle searching for server-side processing - var throttledFun = $.fn.dataTable.util.throttle(fun, options.searchDelay); - $input.on('input', function(e, immediate) { - // always bypass throttling when immediate = true (via the updateSearch method) - (immediate || !server) ? fun() : throttledFun(); - }); - } else if (inArray(type, ['number', 'integer', 'date', 'time'])) { - var $x0 = $x; - $x = $x0.children('div').first(); - $x0.css({ - 'background-color': '#fff', - 'border': '1px #ddd solid', - 'border-radius': '4px', - 'padding': data.vertical ? '35px 20px': '20px 20px 10px 20px' - }); - var $spans = $x0.children('span').css({ - 'margin-top': data.vertical ? '0' : '10px', - 'white-space': 'nowrap' - }); - var $span1 = $spans.first(), $span2 = $spans.last(); - var r1 = +$x.data('min'), r2 = +$x.data('max'); - // when the numbers are too small or have many decimal places, the - // slider may have numeric precision problems (#150) - var scale = Math.pow(10, Math.max(0, +$x.data('scale') || 0)); - r1 = Math.round(r1 * scale); r2 = Math.round(r2 * scale); - var scaleBack = function(x, scale) { - if (scale === 1) return x; - var d = Math.round(Math.log(scale) / Math.log(10)); - // to avoid problems like 3.423/100 -> 0.034230000000000003 - return (x / scale).toFixed(d); - }; - var slider_min = function() { - return filter.noUiSlider('options').range.min; - }; - var slider_max = function() { - return filter.noUiSlider('options').range.max; - }; - $input.on({ - focus: function() { - $x0.show().trigger('show'); - // first, make sure the slider div leaves at least 20px between - // the two (slider value) span's - $x0.width(Math.max(160, $span1.outerWidth() + $span2.outerWidth() + 20)); - // then, if the input is really wide or slider is vertical, - // make the slider the same width as the input - if ($x0.outerWidth() < $input.outerWidth() || data.vertical) { - $x0.outerWidth($input.outerWidth()); - } - // make sure the slider div does not reach beyond the right margin - if ($(window).width() < $x0.offset().left + $x0.width()) { - $x0.offset({ - 'left': $input.offset().left + $input.outerWidth() - $x0.outerWidth() - }); - } - }, - blur: function() { - $x0.hide().trigger('hide'); - }, - input: function() { - if ($input.val() === '') filter.val([slider_min(), slider_max()]); - }, - change: function() { - var v = $input.val().replace(/\s/g, ''); - if (v === '') return; - v = v.split('...'); - if (v.length !== 2) { - $input.parent().addClass('has-error'); - return; - } - if (v[0] === '') v[0] = slider_min(); - if (v[1] === '') v[1] = slider_max(); - $input.parent().removeClass('has-error'); - // treat date as UTC time at midnight - var strTime = function(x) { - var s = type === 'date' ? 'T00:00:00Z' : ''; - var t = new Date(x + s).getTime(); - // add 10 minutes to date since it does not hurt the date, and - // it helps avoid the tricky floating point arithmetic problems, - // e.g. sometimes the date may be a few milliseconds earlier - // than the midnight due to precision problems in noUiSlider - return type === 'date' ? t + 3600000 : t; - }; - if (inArray(type, ['date', 'time'])) { - v[0] = strTime(v[0]); - v[1] = strTime(v[1]); - } - if (v[0] != slider_min()) v[0] *= scale; - if (v[1] != slider_max()) v[1] *= scale; - filter.val(v); - } - }); - var formatDate = function(d) { - d = scaleBack(d, scale); - if (type === 'number') return d; - if (type === 'integer') return parseInt(d); - var x = new Date(+d); - if (type === 'date') { - var pad0 = function(x) { - return ('0' + x).substr(-2, 2); - }; - return x.getUTCFullYear() + '-' + pad0(1 + x.getUTCMonth()) - + '-' + pad0(x.getUTCDate()); - } else { - return x.toISOString(); - } - }; - var opts = type === 'date' ? { step: 60 * 60 * 1000 } : - type === 'integer' ? { step: 1 } : {}; - - opts.orientation = data.vertical ? 'vertical': 'horizontal'; - opts.direction = data.vertical ? 'rtl': 'ltr'; - - filter = $x.noUiSlider($.extend({ - start: [r1, r2], - range: {min: r1, max: r2}, - connect: true - }, opts, data.filterSettings.slider)); - if (scale > 1) (function() { - var t1 = r1, t2 = r2; - var val = filter.val(); - while (val[0] > r1 || val[1] < r2) { - if (val[0] > r1) { - t1 -= val[0] - r1; - } - if (val[1] < r2) { - t2 += r2 - val[1]; - } - filter = $x.noUiSlider($.extend({ - start: [t1, t2], - range: {min: t1, max: t2}, - connect: true - }, opts, data.filterSettings.slider), true); - val = filter.val(); - } - r1 = t1; r2 = t2; - })(); - // format with active column renderer, if defined - var colDef = data.options.columnDefs.find(function(def) { - return (def.targets === i || inArray(i, def.targets)) && 'render' in def; - }); - var updateSliderText = function(v1, v2) { - // we only know how to use function renderers - if (colDef && typeof colDef.render === 'function') { - var restore = function(v) { - v = scaleBack(v, scale); - return inArray(type, ['date', 'time']) ? new Date(+v) : v; - } - $span1.text(colDef.render(restore(v1), 'display')); - $span2.text(colDef.render(restore(v2), 'display')); - } else { - $span1.text(formatDate(v1)); - $span2.text(formatDate(v2)); - } - }; - updateSliderText(r1, r2); - var updateSlider = function(e) { - var val = filter.val(); - // turn off filter if in full range - $td.data('filter', val[0] > slider_min() || val[1] < slider_max()); - var v1 = formatDate(val[0]), v2 = formatDate(val[1]), ival; - if ($td.data('filter')) { - ival = v1 + ' ... ' + v2; - $input.attr('title', ival).val(ival).trigger('input'); - } else { - $input.attr('title', '').val(''); - } - updateSliderText(val[0], val[1]); - if (e.type === 'slide') return; // no searching when sliding only - if (server) { - searchColumn(i, $td.data('filter') ? ival : '').draw(); - return; - } - table.draw(); - }; - filter.on({ - set: updateSlider, - slide: updateSlider - }); - } - - // server-side processing will be handled by R (or whatever server - // language you use); the following code is only needed for client-side - // processing - if (server) { - // if a search string has been pre-set, search now - if (searchCol) $input.trigger('input').trigger('change'); - return; - } - - var customFilter = function(settings, data, dataIndex) { - // there is no way to attach a search function to a specific table, - // and we need to make sure a global search function is not applied to - // all tables (i.e. a range filter in a previous table should not be - // applied to the current table); we use the settings object to - // determine if we want to perform searching on the current table, - // since settings.sTableId will be different to different tables - if (table.settings()[0] !== settings) return true; - // no filter on this column or no need to filter this column - if (typeof filter === 'undefined' || !$td.data('filter')) return true; - - var r = filter.val(), v, r0, r1; - var i_data = function(i) { - if (!colReorderEnabled()) return i; - var order = table.colReorder.order(), k; - for (k = 0; k < order.length; ++k) if (order[k] === i) return k; - return i; // in theory it will never be here... - } - v = data[i_data(i)]; - if (type === 'number' || type === 'integer') { - v = parseFloat(v); - // how to handle NaN? currently exclude these rows - if (isNaN(v)) return(false); - r0 = parseFloat(scaleBack(r[0], scale)) - r1 = parseFloat(scaleBack(r[1], scale)); - if (v >= r0 && v <= r1) return true; - } else if (type === 'date' || type === 'time') { - v = new Date(v); - r0 = new Date(r[0] / scale); r1 = new Date(r[1] / scale); - if (v >= r0 && v <= r1) return true; - } else if (type === 'factor') { - if (r.length === 0 || inArray(v, r)) return true; - } else if (type === 'logical') { - if (r.length === 0) return true; - if (inArray(v === '' ? 'na' : v, r)) return true; - } - return false; - }; - - $.fn.dataTable.ext.search.push(customFilter); - - // search for the preset search strings if it is non-empty - if (searchCol) $input.trigger('input').trigger('change'); - - }); - - } - - // highlight search keywords - var highlight = function() { - var body = $(table.table().body()); - // removing the old highlighting first - body.unhighlight(); - - // don't highlight the "not found" row, so we get the rows using the api - if (table.rows({ filter: 'applied' }).data().length === 0) return; - // highlight global search keywords - body.highlight($.trim(table.search()).split(/\s+/)); - // then highlight keywords from individual column filters - if (filterRow) filterRow.each(function(i, td) { - var $td = $(td), type = $td.data('type'); - if (type !== 'character') return; - var $input = $td.children('div').first().children('input'); - var column = table.column(i).nodes().to$(), - val = $.trim($input.val()); - if (type !== 'character' || val === '') return; - column.highlight(val.split(/\s+/)); - }); - }; - - if (options.searchHighlight) { - table - .on('draw.dt.dth column-visibility.dt.dth column-reorder.dt.dth', highlight) - .on('destroy', function() { - // remove event handler - table.off('draw.dt.dth column-visibility.dt.dth column-reorder.dt.dth'); - }); - - // Set the option for escaping regex characters in our search string. This will be used - // for all future matching. - jQuery.fn.highlight.options.escapeRegex = (!options.search || !options.search.regex); - - // initial highlight for state saved conditions and initial states - highlight(); - } - - // run the callback function on the table instance - if (typeof data.callback === 'function') data.callback(table); - - // double click to edit the cell, row, column, or all cells - if (data.editable) table.on('dblclick.dt', 'tbody td', function(e) { - // only bring up the editor when the cell itself is dbclicked, and ignore - // other dbclick events bubbled up (e.g. from the ) - if (e.target !== this) return; - var target = [], immediate = false; - switch (data.editable.target) { - case 'cell': - target = [this]; - immediate = true; // edit will take effect immediately - break; - case 'row': - target = table.cells(table.cell(this).index().row, '*').nodes(); - break; - case 'column': - target = table.cells('*', table.cell(this).index().column).nodes(); - break; - case 'all': - target = table.cells().nodes(); - break; - default: - throw 'The editable parameter must be "cell", "row", "column", or "all"'; - } - var disableCols = data.editable.disable ? data.editable.disable.columns : null; - var numericCols = data.editable.numeric; - var areaCols = data.editable.area; - var dateCols = data.editable.date; - for (var i = 0; i < target.length; i++) { - (function(cell, current) { - var $cell = $(cell), html = $cell.html(); - var _cell = table.cell(cell), value = _cell.data(), index = _cell.index().column; - var $input; - if (inArray(index, numericCols)) { - $input = $(''); - } else if (inArray(index, areaCols)) { - $input = $(''); - } else if (inArray(index, dateCols)) { - $input = $(''); - } else { - $input = $(''); - } - if (!immediate) { - $cell.data('input', $input).data('html', html); - $input.attr('title', 'Hit Ctrl+Enter to finish editing, or Esc to cancel'); - } - $input.val(value); - if (inArray(index, disableCols)) { - $input.attr('readonly', '').css('filter', 'invert(25%)'); - } - $cell.empty().append($input); - if (cell === current) $input.focus(); - $input.css('width', '100%'); - - if (immediate) $input.on('blur', function(e) { - var valueNew = $input.val(); - if (valueNew !== value) { - _cell.data(valueNew); - if (HTMLWidgets.shinyMode) { - changeInput('cell_edit', [cellInfo(cell)], 'DT.cellInfo', null, {priority: 'event'}); - } - // for server-side processing, users have to call replaceData() to update the table - if (!server) table.draw(false); - } else { - $cell.html(html); - } - }).on('keyup', function(e) { - // hit Escape to cancel editing - if (e.keyCode === 27) $input.trigger('blur'); - }); - - // bulk edit (row, column, or all) - if (!immediate) $input.on('keyup', function(e) { - var removeInput = function($cell, restore) { - $cell.data('input').remove(); - if (restore) $cell.html($cell.data('html')); - } - if (e.keyCode === 27) { - for (var i = 0; i < target.length; i++) { - removeInput($(target[i]), true); - } - } else if (e.keyCode === 13 && e.ctrlKey) { - // Ctrl + Enter - var cell, $cell, _cell, cellData = []; - for (var i = 0; i < target.length; i++) { - cell = target[i]; $cell = $(cell); _cell = table.cell(cell); - _cell.data($cell.data('input').val()); - HTMLWidgets.shinyMode && cellData.push(cellInfo(cell)); - removeInput($cell, false); - } - if (HTMLWidgets.shinyMode) { - changeInput('cell_edit', cellData, 'DT.cellInfo', null, {priority: "event"}); - } - if (!server) table.draw(false); - } - }); - })(target[i], this); - } - }); - - // interaction with shiny - if (!HTMLWidgets.shinyMode && !crosstalkOptions.group) return; - - var methods = {}; - var shinyData = {}; - - methods.updateCaption = function(caption) { - if (!caption) return; - $table.children('caption').replaceWith(caption); - } - - // register clear functions to remove input values when the table is removed - instance.clearInputs = {}; - - var changeInput = function(id, value, type, noCrosstalk, opts) { - var event = id; - id = el.id + '_' + id; - if (type) id = id + ':' + type; - // do not update if the new value is the same as old value - if (event !== 'cell_edit' && !/_clicked$/.test(event) && shinyData.hasOwnProperty(id) && shinyData[id] === JSON.stringify(value)) - return; - shinyData[id] = JSON.stringify(value); - if (HTMLWidgets.shinyMode && Shiny.setInputValue) { - Shiny.setInputValue(id, value, opts); - if (!instance.clearInputs[id]) instance.clearInputs[id] = function() { - Shiny.setInputValue(id, null); - } - } - - // HACK - if (event === "rows_selected" && !noCrosstalk) { - if (crosstalkOptions.group) { - var keys = crosstalkOptions.key; - var selectedKeys = null; - if (value) { - selectedKeys = []; - for (var i = 0; i < value.length; i++) { - // The value array's contents use 1-based row numbers, so we must - // convert to 0-based before indexing into the keys array. - selectedKeys.push(keys[value[i] - 1]); - } - } - instance.ctselectHandle.set(selectedKeys); - } - } - }; - - var addOne = function(x) { - return x.map(function(i) { return 1 + i; }); - }; - - var unique = function(x) { - var ux = []; - $.each(x, function(i, el){ - if ($.inArray(el, ux) === -1) ux.push(el); - }); - return ux; - } - - // change the row index of a cell - var tweakCellIndex = function(cell) { - var info = cell.index(); - // some cell may not be valid. e.g, #759 - // when using the RowGroup extension, datatables will - // generate the row label and the cells are not part of - // the data thus contain no row/col info - if (info === undefined) - return {row: null, col: null}; - if (server) { - info.row = DT_rows_current[info.row]; - } else { - info.row += 1; - } - return {row: info.row, col: info.column}; - } - - var cleanSelectedValues = function() { - changeInput('rows_selected', []); - changeInput('columns_selected', []); - changeInput('cells_selected', transposeArray2D([]), 'shiny.matrix'); - } - // #828 we should clean the selection on the server-side when the table reloads - cleanSelectedValues(); - - // a flag to indicates if select extension is initialized or not - var flagSelectExt = table.settings()[0]._select !== undefined; - // the Select extension should only be used in the client mode and - // when the selection.mode is set to none - if (data.selection.mode === 'none' && !server && flagSelectExt) { - var updateRowsSelected = function() { - var rows = table.rows({selected: true}); - var selected = []; - $.each(rows.indexes().toArray(), function(i, v) { - selected.push(v + 1); - }); - changeInput('rows_selected', selected); - } - var updateColsSelected = function() { - var columns = table.columns({selected: true}); - changeInput('columns_selected', columns.indexes().toArray()); - } - var updateCellsSelected = function() { - var cells = table.cells({selected: true}); - var selected = []; - cells.every(function() { - var row = this.index().row; - var col = this.index().column; - selected = selected.concat([[row + 1, col]]); - }); - changeInput('cells_selected', transposeArray2D(selected), 'shiny.matrix'); - } - table.on('select deselect', function(e, dt, type, indexes) { - updateRowsSelected(); - updateColsSelected(); - updateCellsSelected(); - }) - updateRowsSelected(); - updateColsSelected(); - updateCellsSelected(); - } - - var selMode = data.selection.mode, selTarget = data.selection.target; - var selDisable = data.selection.selectable === false; - if (inArray(selMode, ['single', 'multiple'])) { - var selClass = inArray(data.style, ['bootstrap', 'bootstrap4']) ? 'active' : 'selected'; - // selected1: row indices; selected2: column indices - var initSel = function(x) { - if (x === null || typeof x === 'boolean' || selTarget === 'cell') { - return {rows: [], cols: []}; - } else if (selTarget === 'row') { - return {rows: $.makeArray(x), cols: []}; - } else if (selTarget === 'column') { - return {rows: [], cols: $.makeArray(x)}; - } else if (selTarget === 'row+column') { - return {rows: $.makeArray(x.rows), cols: $.makeArray(x.cols)}; - } - } - var selected = data.selection.selected; - var selected1 = initSel(selected).rows, selected2 = initSel(selected).cols; - // selectable should contain either all positive or all non-positive values, not both - // positive values indicate "selectable" while non-positive values means "nonselectable" - // the assertion is performed on R side. (only column indicides could be zero which indicates - // the row name) - var selectable = data.selection.selectable; - var selectable1 = initSel(selectable).rows, selectable2 = initSel(selectable).cols; - - // After users reorder the rows or filter the table, we cannot use the table index - // directly. Instead, we need this function to find out the rows between the two clicks. - // If user filter the table again between the start click and the end click, the behavior - // would be undefined, but it should not be a problem. - var shiftSelRowsIndex = function(start, end) { - var indexes = server ? DT_rows_all : table.rows({ search: 'applied' }).indexes().toArray(); - start = indexes.indexOf(start); end = indexes.indexOf(end); - // if start is larger than end, we need to swap - if (start > end) { - var tmp = end; end = start; start = tmp; - } - return indexes.slice(start, end + 1); - } - - var serverRowIndex = function(clientRowIndex) { - return server ? DT_rows_current[clientRowIndex] : clientRowIndex + 1; - } - - // row, column, or cell selection - var lastClickedRow; - if (inArray(selTarget, ['row', 'row+column'])) { - // Get the current selected rows. It will also - // update the selected1's value based on the current row selection state - // Note we can't put this function inside selectRows() directly, - // the reason is method.selectRows() will override selected1's value but this - // function will add rows to selected1 (keep the existing selection), which is - // inconsistent with column and cell selection. - var selectedRows = function() { - var rows = table.rows('.' + selClass); - var idx = rows.indexes().toArray(); - if (!server) { - selected1 = addOne(idx); - return selected1; - } - idx = idx.map(function(i) { - return DT_rows_current[i]; - }); - selected1 = selMode === 'multiple' ? unique(selected1.concat(idx)) : idx; - return selected1; - } - // Change selected1's value based on selectable1, then refresh the row state - var onlyKeepSelectableRows = function() { - if (selDisable) { // users can't select; useful when only want backend select - selected1 = []; - return; - } - if (selectable1.length === 0) return; - var nonselectable = selectable1[0] <= 0; - if (nonselectable) { - // should make selectable1 positive - selected1 = $(selected1).not(selectable1.map(function(i) { return -i; })).get(); - } else { - selected1 = $(selected1).filter(selectable1).get(); - } - } - // Change selected1's value based on selectable1, then - // refresh the row selection state according to values in selected1 - var selectRows = function(ignoreSelectable) { - if (!ignoreSelectable) onlyKeepSelectableRows(); - table.$('tr.' + selClass).removeClass(selClass); - if (selected1.length === 0) return; - if (server) { - table.rows({page: 'current'}).every(function() { - if (inArray(DT_rows_current[this.index()], selected1)) { - $(this.node()).addClass(selClass); - } - }); - } else { - var selected0 = selected1.map(function(i) { return i - 1; }); - $(table.rows(selected0).nodes()).addClass(selClass); - } - } - table.on('mousedown.dt', 'tbody tr', function(e) { - var $this = $(this), thisRow = table.row(this); - if (selMode === 'multiple') { - if (e.shiftKey && lastClickedRow !== undefined) { - // select or de-select depends on the last clicked row's status - var flagSel = !$this.hasClass(selClass); - var crtClickedRow = serverRowIndex(thisRow.index()); - if (server) { - var rowsIndex = shiftSelRowsIndex(lastClickedRow, crtClickedRow); - // update current page's selClass - rowsIndex.map(function(i) { - var rowIndex = DT_rows_current.indexOf(i); - if (rowIndex >= 0) { - var row = table.row(rowIndex).nodes().to$(); - var flagRowSel = !row.hasClass(selClass); - if (flagSel === flagRowSel) row.toggleClass(selClass); - } - }); - // update selected1 - if (flagSel) { - selected1 = unique(selected1.concat(rowsIndex)); - } else { - selected1 = selected1.filter(function(index) { - return !inArray(index, rowsIndex); - }); - } - } else { - // js starts from 0 - shiftSelRowsIndex(lastClickedRow - 1, crtClickedRow - 1).map(function(value) { - var row = table.row(value).nodes().to$(); - var flagRowSel = !row.hasClass(selClass); - if (flagSel === flagRowSel) row.toggleClass(selClass); - }); - } - e.preventDefault(); - } else { - $this.toggleClass(selClass); - } - } else { - if ($this.hasClass(selClass)) { - $this.removeClass(selClass); - } else { - table.$('tr.' + selClass).removeClass(selClass); - $this.addClass(selClass); - } - } - if (server && !$this.hasClass(selClass)) { - var id = DT_rows_current[thisRow.index()]; - // remove id from selected1 since its class .selected has been removed - if (inArray(id, selected1)) selected1.splice($.inArray(id, selected1), 1); - } - selectedRows(); // update selected1's value based on selClass - selectRows(false); // only keep the selectable rows - changeInput('rows_selected', selected1); - changeInput('row_last_clicked', serverRowIndex(thisRow.index()), null, null, {priority: 'event'}); - lastClickedRow = serverRowIndex(thisRow.index()); - }); - selectRows(false); // in case users have specified pre-selected rows - // restore selected rows after the table is redrawn (e.g. sort/search/page); - // client-side tables will preserve the selections automatically; for - // server-side tables, we have to *real* row indices are in `selected1` - changeInput('rows_selected', selected1); - if (server) table.on('draw.dt', function(e) { selectRows(false); }); - methods.selectRows = function(selected, ignoreSelectable) { - selected1 = $.makeArray(selected); - selectRows(ignoreSelectable); - changeInput('rows_selected', selected1); - } - } - - if (inArray(selTarget, ['column', 'row+column'])) { - if (selTarget === 'row+column') { - $(table.columns().footer()).css('cursor', 'pointer'); - } - // update selected2's value based on selectable2 - var onlyKeepSelectableCols = function() { - if (selDisable) { // users can't select; useful when only want backend select - selected2 = []; - return; - } - if (selectable2.length === 0) return; - var nonselectable = selectable2[0] <= 0; - if (nonselectable) { - // need to make selectable2 positive - selected2 = $(selected2).not(selectable2.map(function(i) { return -i; })).get(); - } else { - selected2 = $(selected2).filter(selectable2).get(); - } - } - // update selected2 and then - // refresh the col selection state according to values in selected2 - var selectCols = function(ignoreSelectable) { - if (!ignoreSelectable) onlyKeepSelectableCols(); - // if selected2 is not a valide index (e.g., larger than the column number) - // table.columns(selected2) will fail and result in a blank table - // this is different from the table.rows(), where the out-of-range indexes - // doesn't affect at all - selected2 = $(selected2).filter(table.columns().indexes()).get(); - table.columns().nodes().flatten().to$().removeClass(selClass); - if (selected2.length > 0) - table.columns(selected2).nodes().flatten().to$().addClass(selClass); - } - var callback = function() { - var colIdx = selTarget === 'column' ? table.cell(this).index().column : - $.inArray(this, table.columns().footer()), - thisCol = $(table.column(colIdx).nodes()); - if (colIdx === -1) return; - if (thisCol.hasClass(selClass)) { - thisCol.removeClass(selClass); - selected2.splice($.inArray(colIdx, selected2), 1); - } else { - if (selMode === 'single') $(table.cells().nodes()).removeClass(selClass); - thisCol.addClass(selClass); - selected2 = selMode === 'single' ? [colIdx] : unique(selected2.concat([colIdx])); - } - selectCols(false); // update selected2 based on selectable - changeInput('columns_selected', selected2); - } - if (selTarget === 'column') { - $(table.table().body()).on('click.dt', 'td', callback); - } else { - $(table.table().footer()).on('click.dt', 'tr th', callback); - } - selectCols(false); // in case users have specified pre-selected columns - changeInput('columns_selected', selected2); - if (server) table.on('draw.dt', function(e) { selectCols(false); }); - methods.selectColumns = function(selected, ignoreSelectable) { - selected2 = $.makeArray(selected); - selectCols(ignoreSelectable); - changeInput('columns_selected', selected2); - } - } - - if (selTarget === 'cell') { - var selected3 = [], selectable3 = []; - if (selected !== null) selected3 = selected; - if (selectable !== null && typeof selectable !== 'boolean') selectable3 = selectable; - var findIndex = function(ij, sel) { - for (var i = 0; i < sel.length; i++) { - if (ij[0] === sel[i][0] && ij[1] === sel[i][1]) return i; - } - return -1; - } - // Change selected3's value based on selectable3, then refresh the cell state - var onlyKeepSelectableCells = function() { - if (selDisable) { // users can't select; useful when only want backend select - selected3 = []; - return; - } - if (selectable3.length === 0) return; - var nonselectable = selectable3[0][0] <= 0; - var out = []; - if (nonselectable) { - selected3.map(function(ij) { - // should make selectable3 positive - if (findIndex([-ij[0], -ij[1]], selectable3) === -1) { out.push(ij); } - }); - } else { - selected3.map(function(ij) { - if (findIndex(ij, selectable3) > -1) { out.push(ij); } - }); - } - selected3 = out; - } - // Change selected3's value based on selectable3, then - // refresh the cell selection state according to values in selected3 - var selectCells = function(ignoreSelectable) { - if (!ignoreSelectable) onlyKeepSelectableCells(); - table.$('td.' + selClass).removeClass(selClass); - if (selected3.length === 0) return; - if (server) { - table.cells({page: 'current'}).every(function() { - var info = tweakCellIndex(this); - if (findIndex([info.row, info.col], selected3) > -1) - $(this.node()).addClass(selClass); - }); - } else { - selected3.map(function(ij) { - $(table.cell(ij[0] - 1, ij[1]).node()).addClass(selClass); - }); - } - }; - table.on('click.dt', 'tbody td', function() { - var $this = $(this), info = tweakCellIndex(table.cell(this)); - if ($this.hasClass(selClass)) { - $this.removeClass(selClass); - selected3.splice(findIndex([info.row, info.col], selected3), 1); - } else { - if (selMode === 'single') $(table.cells().nodes()).removeClass(selClass); - $this.addClass(selClass); - selected3 = selMode === 'single' ? [[info.row, info.col]] : - unique(selected3.concat([[info.row, info.col]])); - } - selectCells(false); // must call this to update selected3 based on selectable3 - changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix'); - }); - selectCells(false); // in case users have specified pre-selected columns - changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix'); - - if (server) table.on('draw.dt', function(e) { selectCells(false); }); - methods.selectCells = function(selected, ignoreSelectable) { - selected3 = selected ? selected : []; - selectCells(ignoreSelectable); - changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix'); - } - } - } - - // expose some table info to Shiny - var updateTableInfo = function(e, settings) { - // TODO: is anyone interested in the page info? - // changeInput('page_info', table.page.info()); - var updateRowInfo = function(id, modifier) { - var idx; - if (server) { - idx = modifier.page === 'current' ? DT_rows_current : DT_rows_all; - } else { - var rows = table.rows($.extend({ - search: 'applied', - page: 'all' - }, modifier)); - idx = addOne(rows.indexes().toArray()); - } - changeInput('rows' + '_' + id, idx); - }; - updateRowInfo('current', {page: 'current'}); - updateRowInfo('all', {}); - } - table.on('draw.dt', updateTableInfo); - updateTableInfo(); - - // state info - table.on('draw.dt column-visibility.dt', function() { - changeInput('state', table.state()); - }); - changeInput('state', table.state()); - - // search info - var updateSearchInfo = function() { - changeInput('search', table.search()); - if (filterRow) changeInput('search_columns', filterRow.toArray().map(function(td) { - return $(td).find('input').first().val(); - })); - } - table.on('draw.dt', updateSearchInfo); - updateSearchInfo(); - - var cellInfo = function(thiz) { - var info = tweakCellIndex(table.cell(thiz)); - info.value = table.cell(thiz).data(); - return info; - } - // the current cell clicked on - table.on('click.dt', 'tbody td', function() { - changeInput('cell_clicked', cellInfo(this), null, null, {priority: 'event'}); - }) - changeInput('cell_clicked', {}); - - // do not trigger table selection when clicking on links unless they have classes - table.on('mousedown.dt', 'tbody td a', function(e) { - if (this.className === '') e.stopPropagation(); - }); - - methods.addRow = function(data, rowname, resetPaging) { - var n = table.columns().indexes().length, d = n - data.length; - if (d === 1) { - data = rowname.concat(data) - } else if (d !== 0) { - console.log(data); - console.log(table.columns().indexes()); - throw 'New data must be of the same length as current data (' + n + ')'; - }; - table.row.add(data).draw(resetPaging); - } - - methods.updateSearch = function(keywords) { - if (keywords.global !== null) - $(table.table().container()).find('input[type=search]').first() - .val(keywords.global).trigger('input'); - var columns = keywords.columns; - if (!filterRow || columns === null) return; - filterRow.toArray().map(function(td, i) { - var v = typeof columns === 'string' ? columns : columns[i]; - if (typeof v === 'undefined') { - console.log('The search keyword for column ' + i + ' is undefined') - return; - } - // Update column search string and values on linked filter widgets. - // 'input' for factor and char filters, 'change' for numeric filters. - $(td).find('input').first().val(v).trigger('input', [true]).trigger('change'); - }); - table.draw(); - } - - methods.hideCols = function(hide, reset) { - if (reset) table.columns().visible(true, false); - table.columns(hide).visible(false); - } - - methods.showCols = function(show, reset) { - if (reset) table.columns().visible(false, false); - table.columns(show).visible(true); - } - - methods.colReorder = function(order, origOrder) { - table.colReorder.order(order, origOrder); - } - - methods.selectPage = function(page) { - if (table.page.info().pages < page || page < 1) { - throw 'Selected page is out of range'; - }; - table.page(page - 1).draw(false); - } - - methods.reloadData = function(resetPaging, clearSelection) { - // empty selections first if necessary - if (methods.selectRows && inArray('row', clearSelection)) methods.selectRows([]); - if (methods.selectColumns && inArray('column', clearSelection)) methods.selectColumns([]); - if (methods.selectCells && inArray('cell', clearSelection)) methods.selectCells([]); - table.ajax.reload(null, resetPaging); - } - - // update table filters (set new limits of sliders) - methods.updateFilters = function(newProps) { - // loop through each filter in the filter row - filterRow.each(function(i, td) { - var k = i; - if (filterRow.length > newProps.length) { - if (i === 0) return; // first column is row names - k = i - 1; - } - // Update the filters to reflect the updated data. - // Allow "falsy" (e.g. NULL) to signify a no-op. - if (newProps[k]) { - setFilterProps(td, newProps[k]); - } - }); - }; - - table.shinyMethods = methods; - }, - resize: function(el, width, height, instance) { - if (instance.data) this.renderValue(el, instance.data, instance); - - // dynamically adjust height if fillContainer = TRUE - if (instance.fillContainer) - this.fillAvailableHeight(el, height); - - this.adjustWidth(el); - }, - - // dynamically set the scroll body to fill available height - // (used with fillContainer = TRUE) - fillAvailableHeight: function(el, availableHeight) { - - // see how much of the table is occupied by header/footer elements - // and use that to compute a target scroll body height - var dtWrapper = $(el).find('div.dataTables_wrapper'); - var dtScrollBody = $(el).find($('div.dataTables_scrollBody')); - var framingHeight = dtWrapper.innerHeight() - dtScrollBody.innerHeight(); - var scrollBodyHeight = availableHeight - framingHeight; - - // we need to set `max-height` to none as datatables library now sets this - // to a fixed height, disabling the ability to resize to fill the window, - // as it will be set to a fixed 100px under such circumstances, e.g., RStudio IDE, - // or FlexDashboard - // see https://github.com/rstudio/DT/issues/951#issuecomment-1026464509 - dtScrollBody.css('max-height', 'none'); - // set the height - dtScrollBody.height(scrollBodyHeight + 'px'); - }, - - // adjust the width of columns; remove the hard-coded widths on table and the - // scroll header when scrollX/Y are enabled - adjustWidth: function(el) { - var $el = $(el), table = $el.data('datatable'); - if (table) table.columns.adjust(); - $el.find('.dataTables_scrollHeadInner').css('width', '') - .children('table').css('margin-left', ''); - } -}); - - if (!HTMLWidgets.shinyMode) return; - - Shiny.addCustomMessageHandler('datatable-calls', function(data) { - var id = data.id; - var el = document.getElementById(id); - var table = el ? $(el).data('datatable') : null; - if (!table) { - console.log("Couldn't find table with id " + id); - return; - } - - var methods = table.shinyMethods, call = data.call; - if (methods[call.method]) { - methods[call.method].apply(table, call.args); - } else { - console.log("Unknown method " + call.method); - } - }); - -})(); diff --git a/site_libs/datatables-css-0.0.0/datatables-crosstalk.css b/site_libs/datatables-css-0.0.0/datatables-crosstalk.css deleted file mode 100644 index bd1159c8..00000000 --- a/site_libs/datatables-css-0.0.0/datatables-crosstalk.css +++ /dev/null @@ -1,32 +0,0 @@ -.dt-crosstalk-fade { - opacity: 0.2; -} - -html body div.DTS div.dataTables_scrollBody { - background: none; -} - - -/* -Fix https://github.com/rstudio/DT/issues/563 -If the `table.display` is set to "block" (e.g., pkgdown), the browser will display -datatable objects strangely. The search panel and the page buttons will still be -in full-width but the table body will be "compact" and shorter. -In therory, having this attributes will affect `dom="t"` -with `display: block` users. But in reality, there should be no one. -We may remove the below lines in the future if the upstream agree to have this there. -See https://github.com/DataTables/DataTablesSrc/issues/160 -*/ - -table.dataTable { - display: table; -} - - -/* -When DTOutput(fill = TRUE), it receives a .html-fill-item class (via htmltools::bindFillRole()), which effectively amounts to `flex: 1 1 auto`. That's mostly fine, but the case where `fillContainer=TRUE`+`height:auto`+`flex-basis:auto` and the container (e.g., a bslib::card()) doesn't have a defined height is a bit problematic since the table wants to fit the parent but the parent wants to fit the table, which results pretty small table height (maybe because there is a minimum height somewhere?). It seems better in this case to impose a 400px height default for the table, which we can do by setting `flex-basis` to 400px (the table is still allowed to grow/shrink when the container has an opinionated height). -*/ - -.html-fill-container > .html-fill-item.datatables { - flex-basis: 400px; -} diff --git a/site_libs/dt-core-1.13.6/css/jquery.dataTables.extra.css b/site_libs/dt-core-1.13.6/css/jquery.dataTables.extra.css deleted file mode 100644 index b2dd141f..00000000 --- a/site_libs/dt-core-1.13.6/css/jquery.dataTables.extra.css +++ /dev/null @@ -1,28 +0,0 @@ -/* Selected rows/cells */ -table.dataTable tr.selected td, table.dataTable td.selected { - background-color: #b0bed9 !important; -} -/* In case of scrollX/Y or FixedHeader */ -.dataTables_scrollBody .dataTables_sizing { - visibility: hidden; -} - -/* The datatables' theme CSS file doesn't define -the color but with white background. It leads to an issue that -when the HTML's body color is set to 'white', the user can't -see the text since the background is white. One case happens in the -RStudio's IDE when inline viewing the DT table inside an Rmd file, -if the IDE theme is set to "Cobalt". - -See https://github.com/rstudio/DT/issues/447 for more info - -This fixes should have little side-effects because all the other elements -of the default theme use the #333 font color. - -TODO: The upstream may use relative colors for both the table background -and the color. It means the table can display well without this patch -then. At that time, we need to remove the below CSS attributes. -*/ -div.datatables { - color: #333; -} diff --git a/site_libs/dt-core-1.13.6/css/jquery.dataTables.min.css b/site_libs/dt-core-1.13.6/css/jquery.dataTables.min.css deleted file mode 100644 index ad59f843..00000000 --- a/site_libs/dt-core-1.13.6/css/jquery.dataTables.min.css +++ /dev/null @@ -1 +0,0 @@ -:root{--dt-row-selected: 13, 110, 253;--dt-row-selected-text: 255, 255, 255;--dt-row-selected-link: 9, 10, 11;--dt-row-stripe: 0, 0, 0;--dt-row-hover: 0, 0, 0;--dt-column-ordering: 0, 0, 0;--dt-html-background: white}:root.dark{--dt-html-background: rgb(33, 37, 41)}table.dataTable td.dt-control{text-align:center;cursor:pointer}table.dataTable td.dt-control:before{display:inline-block;color:rgba(0, 0, 0, 0.5);content:"►"}table.dataTable tr.dt-hasChild td.dt-control:before{content:"▼"}html.dark table.dataTable td.dt-control:before{color:rgba(255, 255, 255, 0.5)}html.dark table.dataTable tr.dt-hasChild td.dt-control:before{color:rgba(255, 255, 255, 0.5)}table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting_asc_disabled,table.dataTable thead>tr>th.sorting_desc_disabled,table.dataTable thead>tr>td.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting_asc_disabled,table.dataTable thead>tr>td.sorting_desc_disabled{cursor:pointer;position:relative;padding-right:26px}table.dataTable thead>tr>th.sorting:before,table.dataTable thead>tr>th.sorting:after,table.dataTable thead>tr>th.sorting_asc:before,table.dataTable thead>tr>th.sorting_asc:after,table.dataTable thead>tr>th.sorting_desc:before,table.dataTable thead>tr>th.sorting_desc:after,table.dataTable thead>tr>th.sorting_asc_disabled:before,table.dataTable thead>tr>th.sorting_asc_disabled:after,table.dataTable thead>tr>th.sorting_desc_disabled:before,table.dataTable thead>tr>th.sorting_desc_disabled:after,table.dataTable thead>tr>td.sorting:before,table.dataTable thead>tr>td.sorting:after,table.dataTable thead>tr>td.sorting_asc:before,table.dataTable thead>tr>td.sorting_asc:after,table.dataTable thead>tr>td.sorting_desc:before,table.dataTable thead>tr>td.sorting_desc:after,table.dataTable thead>tr>td.sorting_asc_disabled:before,table.dataTable thead>tr>td.sorting_asc_disabled:after,table.dataTable thead>tr>td.sorting_desc_disabled:before,table.dataTable thead>tr>td.sorting_desc_disabled:after{position:absolute;display:block;opacity:.125;right:10px;line-height:9px;font-size:.8em}table.dataTable thead>tr>th.sorting:before,table.dataTable thead>tr>th.sorting_asc:before,table.dataTable thead>tr>th.sorting_desc:before,table.dataTable thead>tr>th.sorting_asc_disabled:before,table.dataTable thead>tr>th.sorting_desc_disabled:before,table.dataTable thead>tr>td.sorting:before,table.dataTable thead>tr>td.sorting_asc:before,table.dataTable thead>tr>td.sorting_desc:before,table.dataTable thead>tr>td.sorting_asc_disabled:before,table.dataTable thead>tr>td.sorting_desc_disabled:before{bottom:50%;content:"▲";content:"▲"/""}table.dataTable thead>tr>th.sorting:after,table.dataTable thead>tr>th.sorting_asc:after,table.dataTable thead>tr>th.sorting_desc:after,table.dataTable thead>tr>th.sorting_asc_disabled:after,table.dataTable thead>tr>th.sorting_desc_disabled:after,table.dataTable thead>tr>td.sorting:after,table.dataTable thead>tr>td.sorting_asc:after,table.dataTable thead>tr>td.sorting_desc:after,table.dataTable thead>tr>td.sorting_asc_disabled:after,table.dataTable thead>tr>td.sorting_desc_disabled:after{top:50%;content:"▼";content:"▼"/""}table.dataTable thead>tr>th.sorting_asc:before,table.dataTable thead>tr>th.sorting_desc:after,table.dataTable thead>tr>td.sorting_asc:before,table.dataTable thead>tr>td.sorting_desc:after{opacity:.6}table.dataTable thead>tr>th.sorting_desc_disabled:after,table.dataTable thead>tr>th.sorting_asc_disabled:before,table.dataTable thead>tr>td.sorting_desc_disabled:after,table.dataTable thead>tr>td.sorting_asc_disabled:before{display:none}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}div.dataTables_scrollBody>table.dataTable>thead>tr>th:before,div.dataTables_scrollBody>table.dataTable>thead>tr>th:after,div.dataTables_scrollBody>table.dataTable>thead>tr>td:before,div.dataTables_scrollBody>table.dataTable>thead>tr>td:after{display:none}div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:2px}div.dataTables_processing>div:last-child{position:relative;width:80px;height:15px;margin:1em auto}div.dataTables_processing>div:last-child>div{position:absolute;top:0;width:13px;height:13px;border-radius:50%;background:rgb(13, 110, 253);background:rgb(var(--dt-row-selected));animation-timing-function:cubic-bezier(0, 1, 1, 0)}div.dataTables_processing>div:last-child>div:nth-child(1){left:8px;animation:datatables-loader-1 .6s infinite}div.dataTables_processing>div:last-child>div:nth-child(2){left:8px;animation:datatables-loader-2 .6s infinite}div.dataTables_processing>div:last-child>div:nth-child(3){left:32px;animation:datatables-loader-2 .6s infinite}div.dataTables_processing>div:last-child>div:nth-child(4){left:56px;animation:datatables-loader-3 .6s infinite}@keyframes datatables-loader-1{0%{transform:scale(0)}100%{transform:scale(1)}}@keyframes datatables-loader-3{0%{transform:scale(1)}100%{transform:scale(0)}}@keyframes datatables-loader-2{0%{transform:translate(0, 0)}100%{transform:translate(24px, 0)}}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th,table.dataTable thead td,table.dataTable tfoot th,table.dataTable tfoot td{text-align:left}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable>thead>tr>th,table.dataTable>thead>tr>td{padding:10px;border-bottom:1px solid rgba(0, 0, 0, 0.3)}table.dataTable>thead>tr>th:active,table.dataTable>thead>tr>td:active{outline:none}table.dataTable>tfoot>tr>th,table.dataTable>tfoot>tr>td{padding:10px 10px 6px 10px;border-top:1px solid rgba(0, 0, 0, 0.3)}table.dataTable tbody tr{background-color:transparent}table.dataTable tbody tr.selected>*{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.9);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.9);color:rgb(255, 255, 255);color:rgb(var(--dt-row-selected-text))}table.dataTable tbody tr.selected a{color:rgb(9, 10, 11);color:rgb(var(--dt-row-selected-link))}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border>tbody>tr>th,table.dataTable.row-border>tbody>tr>td,table.dataTable.display>tbody>tr>th,table.dataTable.display>tbody>tr>td{border-top:1px solid rgba(0, 0, 0, 0.15)}table.dataTable.row-border>tbody>tr:first-child>th,table.dataTable.row-border>tbody>tr:first-child>td,table.dataTable.display>tbody>tr:first-child>th,table.dataTable.display>tbody>tr:first-child>td{border-top:none}table.dataTable.row-border>tbody>tr.selected+tr.selected>td,table.dataTable.display>tbody>tr.selected+tr.selected>td{border-top-color:#0262ef}table.dataTable.cell-border>tbody>tr>th,table.dataTable.cell-border>tbody>tr>td{border-top:1px solid rgba(0, 0, 0, 0.15);border-right:1px solid rgba(0, 0, 0, 0.15)}table.dataTable.cell-border>tbody>tr>th:first-child,table.dataTable.cell-border>tbody>tr>td:first-child{border-left:1px solid rgba(0, 0, 0, 0.15)}table.dataTable.cell-border>tbody>tr:first-child>th,table.dataTable.cell-border>tbody>tr:first-child>td{border-top:none}table.dataTable.stripe>tbody>tr.odd>*,table.dataTable.display>tbody>tr.odd>*{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.023);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.023)}table.dataTable.stripe>tbody>tr.odd.selected>*,table.dataTable.display>tbody>tr.odd.selected>*{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.923);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.923)}table.dataTable.hover>tbody>tr:hover>*,table.dataTable.display>tbody>tr:hover>*{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.035);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.035)}table.dataTable.hover>tbody>tr.selected:hover>*,table.dataTable.display>tbody>tr.selected:hover>*{box-shadow:inset 0 0 0 9999px #0d6efd !important;box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 1) !important}table.dataTable.order-column>tbody tr>.sorting_1,table.dataTable.order-column>tbody tr>.sorting_2,table.dataTable.order-column>tbody tr>.sorting_3,table.dataTable.display>tbody tr>.sorting_1,table.dataTable.display>tbody tr>.sorting_2,table.dataTable.display>tbody tr>.sorting_3{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.019);box-shadow:inset 0 0 0 9999px rgba(var(--dt-column-ordering), 0.019)}table.dataTable.order-column>tbody tr.selected>.sorting_1,table.dataTable.order-column>tbody tr.selected>.sorting_2,table.dataTable.order-column>tbody tr.selected>.sorting_3,table.dataTable.display>tbody tr.selected>.sorting_1,table.dataTable.display>tbody tr.selected>.sorting_2,table.dataTable.display>tbody tr.selected>.sorting_3{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.919);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.919)}table.dataTable.display>tbody>tr.odd>.sorting_1,table.dataTable.order-column.stripe>tbody>tr.odd>.sorting_1{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.054);box-shadow:inset 0 0 0 9999px rgba(var(--dt-column-ordering), 0.054)}table.dataTable.display>tbody>tr.odd>.sorting_2,table.dataTable.order-column.stripe>tbody>tr.odd>.sorting_2{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.047);box-shadow:inset 0 0 0 9999px rgba(var(--dt-column-ordering), 0.047)}table.dataTable.display>tbody>tr.odd>.sorting_3,table.dataTable.order-column.stripe>tbody>tr.odd>.sorting_3{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.039);box-shadow:inset 0 0 0 9999px rgba(var(--dt-column-ordering), 0.039)}table.dataTable.display>tbody>tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe>tbody>tr.odd.selected>.sorting_1{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.954);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.954)}table.dataTable.display>tbody>tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe>tbody>tr.odd.selected>.sorting_2{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.947);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.947)}table.dataTable.display>tbody>tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe>tbody>tr.odd.selected>.sorting_3{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.939);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.939)}table.dataTable.display>tbody>tr.even>.sorting_1,table.dataTable.order-column.stripe>tbody>tr.even>.sorting_1{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.019);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.019)}table.dataTable.display>tbody>tr.even>.sorting_2,table.dataTable.order-column.stripe>tbody>tr.even>.sorting_2{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.011);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.011)}table.dataTable.display>tbody>tr.even>.sorting_3,table.dataTable.order-column.stripe>tbody>tr.even>.sorting_3{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.003);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.003)}table.dataTable.display>tbody>tr.even.selected>.sorting_1,table.dataTable.order-column.stripe>tbody>tr.even.selected>.sorting_1{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.919);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.919)}table.dataTable.display>tbody>tr.even.selected>.sorting_2,table.dataTable.order-column.stripe>tbody>tr.even.selected>.sorting_2{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.911);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.911)}table.dataTable.display>tbody>tr.even.selected>.sorting_3,table.dataTable.order-column.stripe>tbody>tr.even.selected>.sorting_3{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.903);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.903)}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.082);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.082)}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.074);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.074)}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{box-shadow:inset 0 0 0 9999px rgba(0, 0, 0, 0.062);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.062)}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.982);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.982)}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.974);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.974)}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.962);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.962)}table.dataTable.no-footer{border-bottom:1px solid rgba(0, 0, 0, 0.3)}table.dataTable.compact thead th,table.dataTable.compact thead td,table.dataTable.compact tfoot th,table.dataTable.compact tfoot td,table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_length select{border:1px solid #aaa;border-radius:3px;padding:5px;background-color:transparent;color:inherit;padding:4px}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{border:1px solid #aaa;border-radius:3px;padding:5px;background-color:transparent;color:inherit;margin-left:3px}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;color:inherit !important;border:1px solid transparent;border-radius:2px;background:transparent}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:inherit !important;border:1px solid rgba(0, 0, 0, 0.3);background-color:rgba(0, 0, 0, 0.05);background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(230, 230, 230, 0.05)), color-stop(100%, rgba(0, 0, 0, 0.05)));background:-webkit-linear-gradient(top, rgba(230, 230, 230, 0.05) 0%, rgba(0, 0, 0, 0.05) 100%);background:-moz-linear-gradient(top, rgba(230, 230, 230, 0.05) 0%, rgba(0, 0, 0, 0.05) 100%);background:-ms-linear-gradient(top, rgba(230, 230, 230, 0.05) 0%, rgba(0, 0, 0, 0.05) 100%);background:-o-linear-gradient(top, rgba(230, 230, 230, 0.05) 0%, rgba(0, 0, 0, 0.05) 100%);background:linear-gradient(to bottom, rgba(230, 230, 230, 0.05) 0%, rgba(0, 0, 0, 0.05) 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#111;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#0c0c0c;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:inherit}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid rgba(0, 0, 0, 0.3)}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:.5em}}html.dark{--dt-row-hover: 255, 255, 255;--dt-row-stripe: 255, 255, 255;--dt-column-ordering: 255, 255, 255}html.dark table.dataTable>thead>tr>th,html.dark table.dataTable>thead>tr>td{border-bottom:1px solid rgb(89, 91, 94)}html.dark table.dataTable>thead>tr>th:active,html.dark table.dataTable>thead>tr>td:active{outline:none}html.dark table.dataTable>tfoot>tr>th,html.dark table.dataTable>tfoot>tr>td{border-top:1px solid rgb(89, 91, 94)}html.dark table.dataTable.row-border>tbody>tr>th,html.dark table.dataTable.row-border>tbody>tr>td,html.dark table.dataTable.display>tbody>tr>th,html.dark table.dataTable.display>tbody>tr>td{border-top:1px solid rgb(64, 67, 70)}html.dark table.dataTable.row-border>tbody>tr.selected+tr.selected>td,html.dark table.dataTable.display>tbody>tr.selected+tr.selected>td{border-top-color:#0257d5}html.dark table.dataTable.cell-border>tbody>tr>th,html.dark table.dataTable.cell-border>tbody>tr>td{border-top:1px solid rgb(64, 67, 70);border-right:1px solid rgb(64, 67, 70)}html.dark table.dataTable.cell-border>tbody>tr>th:first-child,html.dark table.dataTable.cell-border>tbody>tr>td:first-child{border-left:1px solid rgb(64, 67, 70)}html.dark .dataTables_wrapper .dataTables_filter input,html.dark .dataTables_wrapper .dataTables_length select{border:1px solid rgba(255, 255, 255, 0.2);background-color:var(--dt-html-background)}html.dark .dataTables_wrapper .dataTables_paginate .paginate_button.current,html.dark .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{border:1px solid rgb(89, 91, 94);background:rgba(255, 255, 255, 0.15)}html.dark .dataTables_wrapper .dataTables_paginate .paginate_button.disabled,html.dark .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,html.dark .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{color:#666 !important}html.dark .dataTables_wrapper .dataTables_paginate .paginate_button:hover{border:1px solid rgb(53, 53, 53);background:rgb(53, 53, 53)}html.dark .dataTables_wrapper .dataTables_paginate .paginate_button:active{background:#3a3a3a} diff --git a/site_libs/dt-core-1.13.6/js/jquery.dataTables.min.js b/site_libs/dt-core-1.13.6/js/jquery.dataTables.min.js deleted file mode 100644 index f786b0da..00000000 --- a/site_libs/dt-core-1.13.6/js/jquery.dataTables.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! DataTables 1.13.6 - * ©2008-2023 SpryMedia Ltd - datatables.net/license - */ -!function(n){"use strict";var a;"function"==typeof define&&define.amd?define(["jquery"],function(t){return n(t,window,document)}):"object"==typeof exports?(a=require("jquery"),"undefined"==typeof window?module.exports=function(t,e){return t=t||window,e=e||a(t),n(e,t,t.document)}:n(a,window,window.document)):window.DataTable=n(jQuery,window,document)}(function(P,j,v,H){"use strict";function d(t){var e=parseInt(t,10);return!isNaN(e)&&isFinite(t)?e:null}function l(t,e,n){var a=typeof t,r="string"==a;return"number"==a||"bigint"==a||!!h(t)||(e&&r&&(t=$(t,e)),n&&r&&(t=t.replace(q,"")),!isNaN(parseFloat(t))&&isFinite(t))}function a(t,e,n){var a;return!!h(t)||(h(a=t)||"string"==typeof a)&&!!l(t.replace(V,"").replace(/