just a website
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

Rory Lawless 741033d4

+570
+4
.gitignore
··· 1 + /.quarto/ 2 + **/*.quarto_ipynb 3 + /_site/ 4 + .DS_Store
+3
_about-short.qmd
··· 1 + ## About Me 2 + 3 + I am a data analyst with experience across the non-profit and public sectors. The most important part of my work, especially within my public sector roles, is using data to build deep understanding of the domains I work in, which both informs my own analysis and helps the audience develop meaningful narratives.
+15
_freeze/posts/r-duckdb-and-me/index/execute-results/html.json
··· 1 + { 2 + "hash": "72ff8a5760dd49f80c2a9695ad4b4eb8", 3 + "result": { 4 + "engine": "knitr", 5 + "markdown": "---\ntitle: \"R, DuckDB and Me\"\nauthor: Rory Lawless\ndate: 2025-03-30\nlastmod: 2025-03-31\nformat: html\ndraft: true\n---\n\nOver the past year, [DuckDB](https://duckdb.org/docs/stable/clients/r) has gradually become an important part of my data science workflow - at first clumsily, then seamlessly. I don’t typically work with large datasets, however, integrating DuckDB has addressed some of my frustrations, especially when dealing with hardware limitations and moderately-sized but inefficiently stored data. With this in mind, here are two major benefits I’ve found since integrating DuckDB into my workflow.\n\n## Handling larger-than-memory data\n\nAs noted, I don't work with very large data often but I still run into annoying issues caused by repeated reloading of data after making mistakes - a habit I call Read-Error-Reread (RERe? Let’s make it happen!). Now, this is not an issue for a .csv file containing a few hundred rows and, for larger files or those stored in legacy formats, I could add a \"backup\" step to my code, like so:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ndata <- read.csv(\"some-data-file.csv\")\ndata_backup <- data\n\n# Do some work on data\n\n# Ahh! I made a mistake, let's try again\n\ndata <- data_backup\n```\n:::\n\n\nThis works fine, but it is a bit of an anti-pattern and ought to, in my opinion, be avoided. Instead of adding this extra step - possibly increasing the memory used in the R session - you can use DuckDB to directly query files stored on disk, without having to load them into memory first.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(tidyverse)\nlibrary(duckdb)\n\ncon <- dbConnect(duckdb::duckdb())\n\ndata <- dbGetQuery(\n\tcon,\n\t\"SELECT col_1, col_2, col_4, col_10\n\tFROM 'some-data-file.csv'\n\tWHERE col_10 = 'some_value'\"\n)\n```\n:::\n\n\nThis may seem more complicated at first, and does require some knowledge of SQL, but it is a very efficient way of working with larger datasets, especially in the early stages when you're still exploring the data and working out what you're going to do with it.\n\n## {duckplyr}\n\nA game-changer for me, which really accelerated my adoption of DuckDB as a backend for processing data, was the [{duckplyr}](https://duckplyr.tidyverse.org) package. Those familiar with [{dbplyr}](https://dbplyr.tidyverse.org) will understand the theory behind this package; it allows queries to be built using the standard set of [{dplyr}](https://dplyr.tidyverse.org) functions, which are then converted to SQL behind the scenes. \n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(tidyverse)\nlibrary(duckdb)\nlibrary(duckplyr)\n\ncon <- dbConnect(duckdb::duckdb())\n\npath_to_some_data_file <- \"some-data-file.csv\"\n\ndata <- tbl_file(con, path_to_some_data_file) |>\n\tas_duckdb_tibble() |>\n\tselect(col_1, col_2, col_4, col_10) |>\n\tfilter(col_10 == \"some_value\")\n```\n:::\n\n\nAside from the `tbl_file()`, and `as_duckdb_tibble()` functions, the rest of the code will be familiar to anyone who has used {dplyr} before. The main advantage of using {duckplyr} over writing SQL and using the [{DBI}](https://dbi.r-dbi.org) package is readability - using common {dplyr} functions makes it accessible to a wider range of users. This is a big benefit for teams where not everyone is comfortable reading or writing SQL.\n\nAdditionally, should the original author fall off the face of the earth, the code is still maintainable by others and readily adapted to eliminate the dependency on DuckDB.\n\n## Final thoughts\n\nDuckDB and R are a great combination, allowing me to overcome some of my (self-inflicted?) frustrations in my day-to-day data work. With {duckplyr}, querying data directly from files has smoothed out some of the rough edges in my workflow.\n\n### Update\n\nThe code and text was updated to add the `as_duckdb_tibble()` function that was errorneously missed in the original post.", 6 + "supporting": [], 7 + "filters": [ 8 + "rmarkdown/pagebreak.lua" 9 + ], 10 + "includes": {}, 11 + "engineDependencies": {}, 12 + "preserve": {}, 13 + "postProcess": true 14 + } 15 + }
+15
_freeze/posts/the-basics-of-duckdb-in-r/index/execute-results/html.json
··· 1 + { 2 + "hash": "93302147db836b82be4b7d349d84b1a9", 3 + "result": { 4 + "engine": "knitr", 5 + "markdown": "---\ntitle: \"The basics of DuckDB in R\"\nauthor: Rory Lawless\ndate: 2025-03-30\nlastmod: 2026-01-01\nformat: html\naliases: \n - r-duckdb-and-me.html\n---\n\nOver the past year, [DuckDB](https://duckdb.org/docs/stable/clients/r) has gradually become an important part of my data science workflow - at first clumsily, then seamlessly. I don’t typically work with large datasets, however, integrating DuckDB has addressed some of my frustrations, especially when dealing with hardware limitations and moderately-sized but inefficiently stored data. With this in mind, here are two major benefits I’ve found since integrating DuckDB into my workflow.\n\n## Handling larger-than-memory data\n\nAs noted, I don't work with very large data often but I still run into annoying issues caused by repeated reloading of data after making mistakes. Now, this is not an issue for a .csv file containing a few hundred rows and, for larger files or those stored in legacy formats, I could add a \"backup\" step to my code, like so:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ndata <- read.csv(\"some-data-file.csv\")\ndata_backup <- data\n\n# Do some work on data, maybe make a mistake...\n\ndata <- data_backup\n```\n:::\n\n\nThis works fine, but I consider it an anti-pattern and ought to, in my opinion, be avoided. Instead of adding this extra step - likely increasing the memory used in the R session - you can use DuckDB to directly query files stored on disk, without having to load them into memory first.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(tidyverse)\nlibrary(duckdb)\n\n# Create a DuckDB connection\ncon <- dbConnect(duckdb::duckdb())\n\n# Write a SQL query to read data directly from the CSV file\ndata <- dbGetQuery(\n\tcon,\n\t\"SELECT col_1, col_2, col_4, col_10\n\tFROM 'some-data-file.csv'\n\tWHERE col_10 = 'some_value'\"\n)\n```\n:::\n\n\nThis may seem more complicated at first, and does require some knowledge of SQL, but it is a very efficient way of working with larger datasets, especially in the early stages when you're still exploring the data and working out what you're going to do with it.\n\n## {duckplyr}\n\nA game-changer for me, which really accelerated my adoption of DuckDB as a backend for processing data, was the [{duckplyr}](https://duckplyr.tidyverse.org) package. Those familiar with [{dbplyr}](https://dbplyr.tidyverse.org) will understand the theory behind this package; it allows queries to be built using the standard set of [{dplyr}](https://dplyr.tidyverse.org) functions, which are converted to SQL behind the scenes. \n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(tidyverse)\nlibrary(duckplyr)\n\n# Read CSV using DuckDB behind the scenes\ndata <- read_csv_duckdb(\"some-data-file.csv\")\n\n# Perform data manipulation using dplyr syntax\ndata <- data |>\n\tselect(col_1, col_2, col_4, col_10) |>\n\tfilter(col_10 == \"some_value\")\n```\n:::\n\n\nAside from the `read_csv_duckdb()` function, the rest of the code will be familiar to anyone who has used {dplyr} before. The main advantage of using {duckplyr} over writing SQL and using the [{DBI}](https://dbi.r-dbi.org) package is readability - using common {dplyr} functions makes it accessible to a wider range of users. This is a big benefit for teams where not everyone is comfortable reading or writing SQL.\n\nAdditionally, should the original author fall off the face of the earth, the code is still maintainable by others and readily adapted to eliminate the dependency on DuckDB.\n\n## Final thoughts\n\nDuckDB and R are a great combination, allowing me to overcome some of my (self-inflicted?) frustrations in my day-to-day data work. With {duckplyr}, querying data directly from files has smoothed out some of the rough edges in my workflow.\n", 6 + "supporting": [], 7 + "filters": [ 8 + "rmarkdown/pagebreak.lua" 9 + ], 10 + "includes": {}, 11 + "engineDependencies": {}, 12 + "preserve": {}, 13 + "postProcess": true 14 + } 15 + }
+17
_freeze/posts/using-1password-secret-references-in-R/index/execute-results/html.json
··· 1 + { 2 + "hash": "061c38d96c7f4c560b958a82d0e2a18f", 3 + "result": { 4 + "engine": "knitr", 5 + "markdown": "---\ntitle: \"Using 1Password Secret References in R\"\nauthor: Rory Lawless\ndate: 2025-11-30\nformat: html\ndraft: true\n---\n\nIf you're like me, you store your API keys in your .Renviron file and forget about them. Not only is it risky behaviour to store them in plaintext, it is also a nightmarish way to manage and rotate out keys when needed.\n\n1Password offers a great solution managing your sensitive data, it even has a dedicated API Credentials item type for people super into taxonomy. The real magic, however, happens when you introduce yourself to [1Password CLI and its handy secret references](https://developer.1password.com/docs/cli/secret-reference-syntax) feature.\n\nI won't go into detail on installation and configuration, [the documentation does a better job than I would](https://developer.1password.com/docs/cli/get-started). Once you have migrated an API key to 1Password, you can refer to the secret using its URI instead of including it in plain text in your .Renvion or R script.\n\nThe URI of a credential comes in this format: `op://<vault-name>/<item-name>/[section-name/]<field-name>`. For example, an Anthropic API key save as an item named \"ClaudeAPI\" inside your 'Private' with the value in a field called 'credential' can be referred to as `op://Private/ClaudeAPI/credential` (note, the `[section-name/]` element is only required if the field is under a named section within the item).\n\nThe URI of your credential can then replace the plain text API key anywhere you were storing it. Using our ClaudeAPI, our key=value in .Renviron would look like:\n\n``` \nANTHROPIC_API_KEY=\"op://Private/ClaudeAPI/credential\"\n```\n\nOur key is stored inside our 1Password vault, safe from prying eyes and accidental exposure. To access this environment variable in a script, we would typically write something like `op://Private/Claude API Key/credential`, while this will technically run you will receive an error as you will be attempting to pass the literal secret reference to your API call. We will need to alter this code in two ways, the first is to use one of [the methods 1Password CLI provides](https://developer.1password.com/docs/cli/secrets-scripts) for loading secrets into code, the one we will use in our R script is `op read`. \n\nThe second change, calling the function `system2()` from base, which lets us run `op read` (or any system command) from our R script. \n\n\n::: {.cell}\n\n```{.r .cell-code}\nsystem2(\n\t\"op\",\n\targs = c(\"read\", shQuote(Sys.getenv(\"ANTHROPIC_API_KEY\"))),\n\tstdout = TRUE\n)\n```\n:::\n", 6 + "supporting": [ 7 + "index_files" 8 + ], 9 + "filters": [ 10 + "rmarkdown/pagebreak.lua" 11 + ], 12 + "includes": {}, 13 + "engineDependencies": {}, 14 + "preserve": {}, 15 + "postProcess": true 16 + } 17 + }
+7
_freeze/site_libs/clipboard/clipboard.min.js
··· 1 + /*! 2 + * clipboard.js v2.0.11 3 + * https://clipboardjs.com/ 4 + * 5 + * Licensed MIT © Zeno Rocha 6 + */ 7 + !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return b}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),r=n.n(e);function c(t){try{return document.execCommand(t)}catch(t){return}}var a=function(t){t=r()(t);return c("cut"),t};function o(t,e){var n,o,t=(n=t,o="rtl"===document.documentElement.getAttribute("dir"),(t=document.createElement("textarea")).style.fontSize="12pt",t.style.border="0",t.style.padding="0",t.style.margin="0",t.style.position="absolute",t.style[o?"right":"left"]="-9999px",o=window.pageYOffset||document.documentElement.scrollTop,t.style.top="".concat(o,"px"),t.setAttribute("readonly",""),t.value=n,t);return e.container.appendChild(t),e=r()(t),c("copy"),t.remove(),e}var f=function(t){var e=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{container:document.body},n="";return"string"==typeof t?n=o(t,e):t instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(null==t?void 0:t.type)?n=o(t.value,e):(n=r()(t),c("copy")),n};function l(t){return(l="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var s=function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{},e=t.action,n=void 0===e?"copy":e,o=t.container,e=t.target,t=t.text;if("copy"!==n&&"cut"!==n)throw new Error('Invalid "action" value, use either "copy" or "cut"');if(void 0!==e){if(!e||"object"!==l(e)||1!==e.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===n&&e.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===n&&(e.hasAttribute("readonly")||e.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes')}return t?f(t,{container:o}):e?"cut"===n?a(e):f(e,{container:o}):void 0};function p(t){return(p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function d(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}function y(t,e){return(y=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function h(n){var o=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(t){return!1}}();return function(){var t,e=v(n);return t=o?(t=v(this).constructor,Reflect.construct(e,arguments,t)):e.apply(this,arguments),e=this,!(t=t)||"object"!==p(t)&&"function"!=typeof t?function(t){if(void 0!==t)return t;throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}(e):t}}function v(t){return(v=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}function m(t,e){t="data-clipboard-".concat(t);if(e.hasAttribute(t))return e.getAttribute(t)}var b=function(){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&y(t,e)}(r,i());var t,e,n,o=h(r);function r(t,e){var n;return function(t){if(!(t instanceof r))throw new TypeError("Cannot call a class as a function")}(this),(n=o.call(this)).resolveOptions(e),n.listenClick(t),n}return t=r,n=[{key:"copy",value:function(t){var e=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{container:document.body};return f(t,e)}},{key:"cut",value:function(t){return a(t)}},{key:"isSupported",value:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:["copy","cut"],t="string"==typeof t?[t]:t,e=!!document.queryCommandSupported;return t.forEach(function(t){e=e&&!!document.queryCommandSupported(t)}),e}}],(e=[{key:"resolveOptions",value:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===p(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=u()(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget,n=this.action(e)||"copy",t=s({action:n,container:this.container,target:this.target(e),text:this.text(e)});this.emit(t?"success":"error",{action:n,text:t,trigger:e,clearSelection:function(){e&&e.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(t){return m("action",t)}},{key:"defaultTarget",value:function(t){t=m("target",t);if(t)return document.querySelector(t)}},{key:"defaultText",value:function(t){return m("text",t)}},{key:"destroy",value:function(){this.listener.destroy()}}])&&d(t.prototype,e),n&&d(t,n),r}()},828:function(t){var e;"undefined"==typeof Element||Element.prototype.matches||((e=Element.prototype).matches=e.matchesSelector||e.mozMatchesSelector||e.msMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector),t.exports=function(t,e){for(;t&&9!==t.nodeType;){if("function"==typeof t.matches&&t.matches(e))return t;t=t.parentNode}}},438:function(t,e,n){var u=n(828);function i(t,e,n,o,r){var i=function(e,n,t,o){return function(t){t.delegateTarget=u(t.target,n),t.delegateTarget&&o.call(e,t)}}.apply(this,arguments);return t.addEventListener(n,i,r),{destroy:function(){t.removeEventListener(n,i,r)}}}t.exports=function(t,e,n,o,r){return"function"==typeof t.addEventListener?i.apply(null,arguments):"function"==typeof n?i.bind(null,document).apply(null,arguments):("string"==typeof t&&(t=document.querySelectorAll(t)),Array.prototype.map.call(t,function(t){return i(t,e,n,o,r)}))}},879:function(t,n){n.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},n.nodeList=function(t){var e=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===e||"[object HTMLCollection]"===e)&&"length"in t&&(0===t.length||n.node(t[0]))},n.string=function(t){return"string"==typeof t||t instanceof String},n.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},370:function(t,e,n){var f=n(879),l=n(438);t.exports=function(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!f.string(e))throw new TypeError("Second argument must be a String");if(!f.fn(n))throw new TypeError("Third argument must be a Function");if(f.node(t))return c=e,a=n,(u=t).addEventListener(c,a),{destroy:function(){u.removeEventListener(c,a)}};if(f.nodeList(t))return o=t,r=e,i=n,Array.prototype.forEach.call(o,function(t){t.addEventListener(r,i)}),{destroy:function(){Array.prototype.forEach.call(o,function(t){t.removeEventListener(r,i)})}};if(f.string(t))return t=t,e=e,n=n,l(document.body,t,e,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList");var o,r,i,u,c,a}},817:function(t){t.exports=function(t){var e,n="SELECT"===t.nodeName?(t.focus(),t.value):"INPUT"===t.nodeName||"TEXTAREA"===t.nodeName?((e=t.hasAttribute("readonly"))||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),e||t.removeAttribute("readonly"),t.value):(t.hasAttribute("contenteditable")&&t.focus(),n=window.getSelection(),(e=document.createRange()).selectNodeContents(t),n.removeAllRanges(),n.addRange(e),n.toString());return n}},279:function(t){function e(){}e.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){var o=this;function r(){o.off(t,r),e.apply(n,arguments)}return r._=e,this.on(t,r,n)},emit:function(t){for(var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;o<r;o++)n[o].fn.apply(n[o].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),o=n[t],r=[];if(o&&e)for(var i=0,u=o.length;i<u;i++)o[i].fn!==e&&o[i].fn._!==e&&r.push(o[i]);return r.length?n[t]=r:delete n[t],this}},t.exports=e,t.exports.TinyEmitter=e}},r={},o.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return o.d(e,{a:e}),e},o.d=function(t,e){for(var n in e)o.o(e,n)&&!o.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},o.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},o(686).default;function o(t){if(r[t])return r[t].exports;var e=r[t]={exports:{}};return n[t](e,e.exports,o),e.exports}var n,r});
+2
_freeze/site_libs/quarto-listing/list.min.js
··· 1 + var List;List=function(){var t={"./src/add-async.js":function(t){t.exports=function(t){return function e(r,n,s){var i=r.splice(0,50);s=(s=s||[]).concat(t.add(i)),r.length>0?setTimeout((function(){e(r,n,s)}),1):(t.update(),n(s))}}},"./src/filter.js":function(t){t.exports=function(t){return t.handlers.filterStart=t.handlers.filterStart||[],t.handlers.filterComplete=t.handlers.filterComplete||[],function(e){if(t.trigger("filterStart"),t.i=1,t.reset.filter(),void 0===e)t.filtered=!1;else{t.filtered=!0;for(var r=t.items,n=0,s=r.length;n<s;n++){var i=r[n];e(i)?i.filtered=!0:i.filtered=!1}}return t.update(),t.trigger("filterComplete"),t.visibleItems}}},"./src/fuzzy-search.js":function(t,e,r){r("./src/utils/classes.js");var n=r("./src/utils/events.js"),s=r("./src/utils/extend.js"),i=r("./src/utils/to-string.js"),a=r("./src/utils/get-by-class.js"),o=r("./src/utils/fuzzy.js");t.exports=function(t,e){e=s({location:0,distance:100,threshold:.4,multiSearch:!0,searchClass:"fuzzy-search"},e=e||{});var r={search:function(n,s){for(var i=e.multiSearch?n.replace(/ +$/,"").split(/ +/):[n],a=0,o=t.items.length;a<o;a++)r.item(t.items[a],s,i)},item:function(t,e,n){for(var s=!0,i=0;i<n.length;i++){for(var a=!1,o=0,l=e.length;o<l;o++)r.values(t.values(),e[o],n[i])&&(a=!0);a||(s=!1)}t.found=s},values:function(t,r,n){if(t.hasOwnProperty(r)){var s=i(t[r]).toLowerCase();if(o(s,n,e))return!0}return!1}};return n.bind(a(t.listContainer,e.searchClass),"keyup",t.utils.events.debounce((function(e){var n=e.target||e.srcElement;t.search(n.value,r.search)}),t.searchDelay)),function(e,n){t.search(e,n,r.search)}}},"./src/index.js":function(t,e,r){var n=r("./node_modules/string-natural-compare/natural-compare.js"),s=r("./src/utils/get-by-class.js"),i=r("./src/utils/extend.js"),a=r("./src/utils/index-of.js"),o=r("./src/utils/events.js"),l=r("./src/utils/to-string.js"),u=r("./src/utils/classes.js"),c=r("./src/utils/get-attribute.js"),f=r("./src/utils/to-array.js");t.exports=function(t,e,h){var d,v=this,g=r("./src/item.js")(v),m=r("./src/add-async.js")(v),p=r("./src/pagination.js")(v);d={start:function(){v.listClass="list",v.searchClass="search",v.sortClass="sort",v.page=1e4,v.i=1,v.items=[],v.visibleItems=[],v.matchingItems=[],v.searched=!1,v.filtered=!1,v.searchColumns=void 0,v.searchDelay=0,v.handlers={updated:[]},v.valueNames=[],v.utils={getByClass:s,extend:i,indexOf:a,events:o,toString:l,naturalSort:n,classes:u,getAttribute:c,toArray:f},v.utils.extend(v,e),v.listContainer="string"==typeof t?document.getElementById(t):t,v.listContainer&&(v.list=s(v.listContainer,v.listClass,!0),v.parse=r("./src/parse.js")(v),v.templater=r("./src/templater.js")(v),v.search=r("./src/search.js")(v),v.filter=r("./src/filter.js")(v),v.sort=r("./src/sort.js")(v),v.fuzzySearch=r("./src/fuzzy-search.js")(v,e.fuzzySearch),this.handlers(),this.items(),this.pagination(),v.update())},handlers:function(){for(var t in v.handlers)v[t]&&v.handlers.hasOwnProperty(t)&&v.on(t,v[t])},items:function(){v.parse(v.list),void 0!==h&&v.add(h)},pagination:function(){if(void 0!==e.pagination){!0===e.pagination&&(e.pagination=[{}]),void 0===e.pagination[0]&&(e.pagination=[e.pagination]);for(var t=0,r=e.pagination.length;t<r;t++)p(e.pagination[t])}}},this.reIndex=function(){v.items=[],v.visibleItems=[],v.matchingItems=[],v.searched=!1,v.filtered=!1,v.parse(v.list)},this.toJSON=function(){for(var t=[],e=0,r=v.items.length;e<r;e++)t.push(v.items[e].values());return t},this.add=function(t,e){if(0!==t.length){if(!e){var r=[],n=!1;void 0===t[0]&&(t=[t]);for(var s=0,i=t.length;s<i;s++){var a;n=v.items.length>v.page,a=new g(t[s],void 0,n),v.items.push(a),r.push(a)}return v.update(),r}m(t.slice(0),e)}},this.show=function(t,e){return this.i=t,this.page=e,v.update(),v},this.remove=function(t,e,r){for(var n=0,s=0,i=v.items.length;s<i;s++)v.items[s].values()[t]==e&&(v.templater.remove(v.items[s],r),v.items.splice(s,1),i--,s--,n++);return v.update(),n},this.get=function(t,e){for(var r=[],n=0,s=v.items.length;n<s;n++){var i=v.items[n];i.values()[t]==e&&r.push(i)}return r},this.size=function(){return v.items.length},this.clear=function(){return v.templater.clear(),v.items=[],v},this.on=function(t,e){return v.handlers[t].push(e),v},this.off=function(t,e){var r=v.handlers[t],n=a(r,e);return n>-1&&r.splice(n,1),v},this.trigger=function(t){for(var e=v.handlers[t].length;e--;)v.handlers[t][e](v);return v},this.reset={filter:function(){for(var t=v.items,e=t.length;e--;)t[e].filtered=!1;return v},search:function(){for(var t=v.items,e=t.length;e--;)t[e].found=!1;return v}},this.update=function(){var t=v.items,e=t.length;v.visibleItems=[],v.matchingItems=[],v.templater.clear();for(var r=0;r<e;r++)t[r].matching()&&v.matchingItems.length+1>=v.i&&v.visibleItems.length<v.page?(t[r].show(),v.visibleItems.push(t[r]),v.matchingItems.push(t[r])):t[r].matching()?(v.matchingItems.push(t[r]),t[r].hide()):t[r].hide();return v.trigger("updated"),v},d.start()}},"./src/item.js":function(t){t.exports=function(t){return function(e,r,n){var s=this;this._values={},this.found=!1,this.filtered=!1;this.values=function(e,r){if(void 0===e)return s._values;for(var n in e)s._values[n]=e[n];!0!==r&&t.templater.set(s,s.values())},this.show=function(){t.templater.show(s)},this.hide=function(){t.templater.hide(s)},this.matching=function(){return t.filtered&&t.searched&&s.found&&s.filtered||t.filtered&&!t.searched&&s.filtered||!t.filtered&&t.searched&&s.found||!t.filtered&&!t.searched},this.visible=function(){return!(!s.elm||s.elm.parentNode!=t.list)},function(e,r,n){if(void 0===r)n?s.values(e,n):s.values(e);else{s.elm=r;var i=t.templater.get(s,e);s.values(i)}}(e,r,n)}}},"./src/pagination.js":function(t,e,r){var n=r("./src/utils/classes.js"),s=r("./src/utils/events.js"),i=r("./src/index.js");t.exports=function(t){var e=!1,r=function(r,s){if(t.page<1)return t.listContainer.style.display="none",void(e=!0);e&&(t.listContainer.style.display="block");var i,o=t.matchingItems.length,l=t.i,u=t.page,c=Math.ceil(o/u),f=Math.ceil(l/u),h=s.innerWindow||2,d=s.left||s.outerWindow||0,v=s.right||s.outerWindow||0;v=c-v,r.clear();for(var g=1;g<=c;g++){var m=f===g?"active":"";a.number(g,d,v,f,h)?(i=r.add({page:g,dotted:!1})[0],m&&n(i.elm).add(m),i.elm.firstChild.setAttribute("data-i",g),i.elm.firstChild.setAttribute("data-page",u)):a.dotted(r,g,d,v,f,h,r.size())&&(i=r.add({page:"...",dotted:!0})[0],n(i.elm).add("disabled"))}},a={number:function(t,e,r,n,s){return this.left(t,e)||this.right(t,r)||this.innerWindow(t,n,s)},left:function(t,e){return t<=e},right:function(t,e){return t>e},innerWindow:function(t,e,r){return t>=e-r&&t<=e+r},dotted:function(t,e,r,n,s,i,a){return this.dottedLeft(t,e,r,n,s,i)||this.dottedRight(t,e,r,n,s,i,a)},dottedLeft:function(t,e,r,n,s,i){return e==r+1&&!this.innerWindow(e,s,i)&&!this.right(e,n)},dottedRight:function(t,e,r,n,s,i,a){return!t.items[a-1].values().dotted&&(e==n&&!this.innerWindow(e,s,i)&&!this.right(e,n))}};return function(e){var n=new i(t.listContainer.id,{listClass:e.paginationClass||"pagination",item:e.item||"<li><a class='page' href='#'></a></li>",valueNames:["page","dotted"],searchClass:"pagination-search-that-is-not-supposed-to-exist",sortClass:"pagination-sort-that-is-not-supposed-to-exist"});s.bind(n.listContainer,"click",(function(e){var r=e.target||e.srcElement,n=t.utils.getAttribute(r,"data-page"),s=t.utils.getAttribute(r,"data-i");s&&t.show((s-1)*n+1,n)})),t.on("updated",(function(){r(n,e)})),r(n,e)}}},"./src/parse.js":function(t,e,r){t.exports=function(t){var e=r("./src/item.js")(t),n=function(r,n){for(var s=0,i=r.length;s<i;s++)t.items.push(new e(n,r[s]))},s=function e(r,s){var i=r.splice(0,50);n(i,s),r.length>0?setTimeout((function(){e(r,s)}),1):(t.update(),t.trigger("parseComplete"))};return t.handlers.parseComplete=t.handlers.parseComplete||[],function(){var e=function(t){for(var e=t.childNodes,r=[],n=0,s=e.length;n<s;n++)void 0===e[n].data&&r.push(e[n]);return r}(t.list),r=t.valueNames;t.indexAsync?s(e,r):n(e,r)}}},"./src/search.js":function(t){t.exports=function(t){var e,r,n,s={resetList:function(){t.i=1,t.templater.clear(),n=void 0},setOptions:function(t){2==t.length&&t[1]instanceof Array?e=t[1]:2==t.length&&"function"==typeof t[1]?(e=void 0,n=t[1]):3==t.length?(e=t[1],n=t[2]):e=void 0},setColumns:function(){0!==t.items.length&&void 0===e&&(e=void 0===t.searchColumns?s.toArray(t.items[0].values()):t.searchColumns)},setSearchString:function(e){e=(e=t.utils.toString(e).toLowerCase()),r=e},toArray:function(t){var e=[];for(var r in t)e.push(r);return e}},i=function(){for(var n,s=[],i=r;null!==(n=i.match(/"([^"]+)"/));)s.push(n[1]),i=i.substring(0,n.index)+i.substring(n.index+n[0].length);(i=i.trim()).length&&(s=s.concat(i.split(/\s+/)));for(var a=0,o=t.items.length;a<o;a++){var l=t.items[a];if(l.found=!1,s.length){for(var u=0,c=s.length;u<c;u++){for(var f=!1,h=0,d=e.length;h<d;h++){var v=l.values(),g=e[h];if(v.hasOwnProperty(g)&&void 0!==v[g]&&null!==v[g])if(-1!==("string"!=typeof v[g]?v[g].toString():v[g]).toLowerCase().indexOf(s[u])){f=!0;break}}if(!f)break}l.found=f}}},a=function(){t.reset.search(),t.searched=!1},o=function(o){return t.trigger("searchStart"),s.resetList(),s.setSearchString(o),s.setOptions(arguments),s.setColumns(),""===r?a():(t.searched=!0,n?n(r,e):i()),t.update(),t.trigger("searchComplete"),t.visibleItems};return t.handlers.searchStart=t.handlers.searchStart||[],t.handlers.searchComplete=t.handlers.searchComplete||[],t.utils.events.bind(t.utils.getByClass(t.listContainer,t.searchClass),"keyup",t.utils.events.debounce((function(e){var r=e.target||e.srcElement;""===r.value&&!t.searched||o(r.value)}),t.searchDelay)),t.utils.events.bind(t.utils.getByClass(t.listContainer,t.searchClass),"input",(function(t){""===(t.target||t.srcElement).value&&o("")})),o}},"./src/sort.js":function(t){t.exports=function(t){var e={els:void 0,clear:function(){for(var r=0,n=e.els.length;r<n;r++)t.utils.classes(e.els[r]).remove("asc"),t.utils.classes(e.els[r]).remove("desc")},getOrder:function(e){var r=t.utils.getAttribute(e,"data-order");return"asc"==r||"desc"==r?r:t.utils.classes(e).has("desc")?"asc":t.utils.classes(e).has("asc")?"desc":"asc"},getInSensitive:function(e,r){var n=t.utils.getAttribute(e,"data-insensitive");r.insensitive="false"!==n},setOrder:function(r){for(var n=0,s=e.els.length;n<s;n++){var i=e.els[n];if(t.utils.getAttribute(i,"data-sort")===r.valueName){var a=t.utils.getAttribute(i,"data-order");"asc"==a||"desc"==a?a==r.order&&t.utils.classes(i).add(r.order):t.utils.classes(i).add(r.order)}}}},r=function(){t.trigger("sortStart");var r={},n=arguments[0].currentTarget||arguments[0].srcElement||void 0;n?(r.valueName=t.utils.getAttribute(n,"data-sort"),e.getInSensitive(n,r),r.order=e.getOrder(n)):((r=arguments[1]||r).valueName=arguments[0],r.order=r.order||"asc",r.insensitive=void 0===r.insensitive||r.insensitive),e.clear(),e.setOrder(r);var s,i=r.sortFunction||t.sortFunction||null,a="desc"===r.order?-1:1;s=i?function(t,e){return i(t,e,r)*a}:function(e,n){var s=t.utils.naturalSort;return s.alphabet=t.alphabet||r.alphabet||void 0,!s.alphabet&&r.insensitive&&(s=t.utils.naturalSort.caseInsensitive),s(e.values()[r.valueName],n.values()[r.valueName])*a},t.items.sort(s),t.update(),t.trigger("sortComplete")};return t.handlers.sortStart=t.handlers.sortStart||[],t.handlers.sortComplete=t.handlers.sortComplete||[],e.els=t.utils.getByClass(t.listContainer,t.sortClass),t.utils.events.bind(e.els,"click",r),t.on("searchStart",e.clear),t.on("filterStart",e.clear),r}},"./src/templater.js":function(t){var e=function(t){var e,r=this,n=function(e,r){var n=e.cloneNode(!0);n.removeAttribute("id");for(var s=0,i=r.length;s<i;s++){var a=void 0,o=r[s];if(o.data)for(var l=0,u=o.data.length;l<u;l++)n.setAttribute("data-"+o.data[l],"");else o.attr&&o.name?(a=t.utils.getByClass(n,o.name,!0))&&a.setAttribute(o.attr,""):(a=t.utils.getByClass(n,o,!0))&&(a.innerHTML="")}return n},s=function(){for(var e=t.list.childNodes,r=0,n=e.length;r<n;r++)if(void 0===e[r].data)return e[r].cloneNode(!0)},i=function(t){if("string"==typeof t){if(/<tr[\s>]/g.exec(t)){var e=document.createElement("tbody");return e.innerHTML=t,e.firstElementChild}if(-1!==t.indexOf("<")){var r=document.createElement("div");return r.innerHTML=t,r.firstElementChild}}},a=function(e,r,n){var s=void 0,i=function(e){for(var r=0,n=t.valueNames.length;r<n;r++){var s=t.valueNames[r];if(s.data){for(var i=s.data,a=0,o=i.length;a<o;a++)if(i[a]===e)return{data:e}}else{if(s.attr&&s.name&&s.name==e)return s;if(s===e)return e}}}(r);i&&(i.data?e.elm.setAttribute("data-"+i.data,n):i.attr&&i.name?(s=t.utils.getByClass(e.elm,i.name,!0))&&s.setAttribute(i.attr,n):(s=t.utils.getByClass(e.elm,i,!0))&&(s.innerHTML=n))};this.get=function(e,n){r.create(e);for(var s={},i=0,a=n.length;i<a;i++){var o=void 0,l=n[i];if(l.data)for(var u=0,c=l.data.length;u<c;u++)s[l.data[u]]=t.utils.getAttribute(e.elm,"data-"+l.data[u]);else l.attr&&l.name?(o=t.utils.getByClass(e.elm,l.name,!0),s[l.name]=o?t.utils.getAttribute(o,l.attr):""):(o=t.utils.getByClass(e.elm,l,!0),s[l]=o?o.innerHTML:"")}return s},this.set=function(t,e){if(!r.create(t))for(var n in e)e.hasOwnProperty(n)&&a(t,n,e[n])},this.create=function(t){return void 0===t.elm&&(t.elm=e(t.values()),r.set(t,t.values()),!0)},this.remove=function(e){e.elm.parentNode===t.list&&t.list.removeChild(e.elm)},this.show=function(e){r.create(e),t.list.appendChild(e.elm)},this.hide=function(e){void 0!==e.elm&&e.elm.parentNode===t.list&&t.list.removeChild(e.elm)},this.clear=function(){if(t.list.hasChildNodes())for(;t.list.childNodes.length>=1;)t.list.removeChild(t.list.firstChild)},function(){var r;if("function"!=typeof t.item){if(!(r="string"==typeof t.item?-1===t.item.indexOf("<")?document.getElementById(t.item):i(t.item):s()))throw new Error("The list needs to have at least one item on init otherwise you'll have to add a template.");r=n(r,t.valueNames),e=function(){return r.cloneNode(!0)}}else e=function(e){var r=t.item(e);return i(r)}}()};t.exports=function(t){return new e(t)}},"./src/utils/classes.js":function(t,e,r){var n=r("./src/utils/index-of.js"),s=/\s+/;Object.prototype.toString;function i(t){if(!t||!t.nodeType)throw new Error("A DOM element reference is required");this.el=t,this.list=t.classList}t.exports=function(t){return new i(t)},i.prototype.add=function(t){if(this.list)return this.list.add(t),this;var e=this.array();return~n(e,t)||e.push(t),this.el.className=e.join(" "),this},i.prototype.remove=function(t){if(this.list)return this.list.remove(t),this;var e=this.array(),r=n(e,t);return~r&&e.splice(r,1),this.el.className=e.join(" "),this},i.prototype.toggle=function(t,e){return this.list?(void 0!==e?e!==this.list.toggle(t,e)&&this.list.toggle(t):this.list.toggle(t),this):(void 0!==e?e?this.add(t):this.remove(t):this.has(t)?this.remove(t):this.add(t),this)},i.prototype.array=function(){var t=(this.el.getAttribute("class")||"").replace(/^\s+|\s+$/g,"").split(s);return""===t[0]&&t.shift(),t},i.prototype.has=i.prototype.contains=function(t){return this.list?this.list.contains(t):!!~n(this.array(),t)}},"./src/utils/events.js":function(t,e,r){var n=window.addEventListener?"addEventListener":"attachEvent",s=window.removeEventListener?"removeEventListener":"detachEvent",i="addEventListener"!==n?"on":"",a=r("./src/utils/to-array.js");e.bind=function(t,e,r,s){for(var o=0,l=(t=a(t)).length;o<l;o++)t[o][n](i+e,r,s||!1)},e.unbind=function(t,e,r,n){for(var o=0,l=(t=a(t)).length;o<l;o++)t[o][s](i+e,r,n||!1)},e.debounce=function(t,e,r){var n;return e?function(){var s=this,i=arguments,a=function(){n=null,r||t.apply(s,i)},o=r&&!n;clearTimeout(n),n=setTimeout(a,e),o&&t.apply(s,i)}:t}},"./src/utils/extend.js":function(t){t.exports=function(t){for(var e,r=Array.prototype.slice.call(arguments,1),n=0;e=r[n];n++)if(e)for(var s in e)t[s]=e[s];return t}},"./src/utils/fuzzy.js":function(t){t.exports=function(t,e,r){var n=r.location||0,s=r.distance||100,i=r.threshold||.4;if(e===t)return!0;if(e.length>32)return!1;var a=n,o=function(){var t,r={};for(t=0;t<e.length;t++)r[e.charAt(t)]=0;for(t=0;t<e.length;t++)r[e.charAt(t)]|=1<<e.length-t-1;return r}();function l(t,r){var n=t/e.length,i=Math.abs(a-r);return s?n+i/s:i?1:n}var u=i,c=t.indexOf(e,a);-1!=c&&(u=Math.min(l(0,c),u),-1!=(c=t.lastIndexOf(e,a+e.length))&&(u=Math.min(l(0,c),u)));var f,h,d=1<<e.length-1;c=-1;for(var v,g=e.length+t.length,m=0;m<e.length;m++){for(f=0,h=g;f<h;)l(m,a+h)<=u?f=h:g=h,h=Math.floor((g-f)/2+f);g=h;var p=Math.max(1,a-h+1),y=Math.min(a+h,t.length)+e.length,C=Array(y+2);C[y+1]=(1<<m)-1;for(var b=y;b>=p;b--){var j=o[t.charAt(b-1)];if(C[b]=0===m?(C[b+1]<<1|1)&j:(C[b+1]<<1|1)&j|(v[b+1]|v[b])<<1|1|v[b+1],C[b]&d){var x=l(m,b-1);if(x<=u){if(u=x,!((c=b-1)>a))break;p=Math.max(1,2*a-c)}}}if(l(m+1,a)>u)break;v=C}return!(c<0)}},"./src/utils/get-attribute.js":function(t){t.exports=function(t,e){var r=t.getAttribute&&t.getAttribute(e)||null;if(!r)for(var n=t.attributes,s=n.length,i=0;i<s;i++)void 0!==n[i]&&n[i].nodeName===e&&(r=n[i].nodeValue);return r}},"./src/utils/get-by-class.js":function(t){t.exports=function(t,e,r,n){return(n=n||{}).test&&n.getElementsByClassName||!n.test&&document.getElementsByClassName?function(t,e,r){return r?t.getElementsByClassName(e)[0]:t.getElementsByClassName(e)}(t,e,r):n.test&&n.querySelector||!n.test&&document.querySelector?function(t,e,r){return e="."+e,r?t.querySelector(e):t.querySelectorAll(e)}(t,e,r):function(t,e,r){for(var n=[],s=t.getElementsByTagName("*"),i=s.length,a=new RegExp("(^|\\s)"+e+"(\\s|$)"),o=0,l=0;o<i;o++)if(a.test(s[o].className)){if(r)return s[o];n[l]=s[o],l++}return n}(t,e,r)}},"./src/utils/index-of.js":function(t){var e=[].indexOf;t.exports=function(t,r){if(e)return t.indexOf(r);for(var n=0,s=t.length;n<s;++n)if(t[n]===r)return n;return-1}},"./src/utils/to-array.js":function(t){t.exports=function(t){if(void 0===t)return[];if(null===t)return[null];if(t===window)return[window];if("string"==typeof t)return[t];if(function(t){return"[object Array]"===Object.prototype.toString.call(t)}(t))return t;if("number"!=typeof t.length)return[t];if("function"==typeof t&&t instanceof Function)return[t];for(var e=[],r=0,n=t.length;r<n;r++)(Object.prototype.hasOwnProperty.call(t,r)||r in t)&&e.push(t[r]);return e.length?e:[]}},"./src/utils/to-string.js":function(t){t.exports=function(t){return t=(t=null===(t=void 0===t?"":t)?"":t).toString()}},"./node_modules/string-natural-compare/natural-compare.js":function(t){"use strict";var e,r,n=0;function s(t){return t>=48&&t<=57}function i(t,e){for(var i=(t+="").length,a=(e+="").length,o=0,l=0;o<i&&l<a;){var u=t.charCodeAt(o),c=e.charCodeAt(l);if(s(u)){if(!s(c))return u-c;for(var f=o,h=l;48===u&&++f<i;)u=t.charCodeAt(f);for(;48===c&&++h<a;)c=e.charCodeAt(h);for(var d=f,v=h;d<i&&s(t.charCodeAt(d));)++d;for(;v<a&&s(e.charCodeAt(v));)++v;var g=d-f-v+h;if(g)return g;for(;f<d;)if(g=t.charCodeAt(f++)-e.charCodeAt(h++))return g;o=d,l=v}else{if(u!==c)return u<n&&c<n&&-1!==r[u]&&-1!==r[c]?r[u]-r[c]:u-c;++o,++l}}return o>=i&&l<a&&i>=a?-1:l>=a&&o<i&&a>=i?1:i-a}i.caseInsensitive=i.i=function(t,e){return i((""+t).toLowerCase(),(""+e).toLowerCase())},Object.defineProperties(i,{alphabet:{get:function(){return e},set:function(t){r=[];var s=0;if(e=t)for(;s<e.length;s++)r[e.charCodeAt(s)]=s;for(n=r.length,s=0;s<n;s++)void 0===r[s]&&(r[s]=-1)}}}),t.exports=i}},e={};return function r(n){if(e[n])return e[n].exports;var s=e[n]={exports:{}};return t[n](s,s.exports,r),s.exports}("./src/index.js")}(); 2 + //# sourceMappingURL=list.min.js.map
+254
_freeze/site_libs/quarto-listing/quarto-listing.js
··· 1 + const kProgressiveAttr = "data-src"; 2 + let categoriesLoaded = false; 3 + 4 + window.quartoListingCategory = (category) => { 5 + // category is URI encoded in EJS template for UTF-8 support 6 + category = decodeURIComponent(atob(category)); 7 + if (categoriesLoaded) { 8 + activateCategory(category); 9 + setCategoryHash(category); 10 + } 11 + }; 12 + 13 + window["quarto-listing-loaded"] = () => { 14 + // Process any existing hash 15 + const hash = getHash(); 16 + 17 + if (hash) { 18 + // If there is a category, switch to that 19 + if (hash.category) { 20 + // category hash are URI encoded so we need to decode it before processing 21 + // so that we can match it with the category element processed in JS 22 + activateCategory(decodeURIComponent(hash.category)); 23 + } 24 + // Paginate a specific listing 25 + const listingIds = Object.keys(window["quarto-listings"]); 26 + for (const listingId of listingIds) { 27 + const page = hash[getListingPageKey(listingId)]; 28 + if (page) { 29 + showPage(listingId, page); 30 + } 31 + } 32 + } 33 + 34 + const listingIds = Object.keys(window["quarto-listings"]); 35 + for (const listingId of listingIds) { 36 + // The actual list 37 + const list = window["quarto-listings"][listingId]; 38 + 39 + // Update the handlers for pagination events 40 + refreshPaginationHandlers(listingId); 41 + 42 + // Render any visible items that need it 43 + renderVisibleProgressiveImages(list); 44 + 45 + // Whenever the list is updated, we also need to 46 + // attach handlers to the new pagination elements 47 + // and refresh any newly visible items. 48 + list.on("updated", function () { 49 + renderVisibleProgressiveImages(list); 50 + setTimeout(() => refreshPaginationHandlers(listingId)); 51 + 52 + // Show or hide the no matching message 53 + toggleNoMatchingMessage(list); 54 + }); 55 + } 56 + }; 57 + 58 + window.document.addEventListener("DOMContentLoaded", function (_event) { 59 + // Attach click handlers to categories 60 + const categoryEls = window.document.querySelectorAll( 61 + ".quarto-listing-category .category" 62 + ); 63 + 64 + for (const categoryEl of categoryEls) { 65 + // category needs to support non ASCII characters 66 + const category = decodeURIComponent( 67 + atob(categoryEl.getAttribute("data-category")) 68 + ); 69 + categoryEl.onclick = () => { 70 + activateCategory(category); 71 + setCategoryHash(category); 72 + }; 73 + } 74 + 75 + // Attach a click handler to the category title 76 + // (there should be only one, but since it is a class name, handle N) 77 + const categoryTitleEls = window.document.querySelectorAll( 78 + ".quarto-listing-category-title" 79 + ); 80 + for (const categoryTitleEl of categoryTitleEls) { 81 + categoryTitleEl.onclick = () => { 82 + activateCategory(""); 83 + setCategoryHash(""); 84 + }; 85 + } 86 + 87 + categoriesLoaded = true; 88 + }); 89 + 90 + function toggleNoMatchingMessage(list) { 91 + const selector = `#${list.listContainer.id} .listing-no-matching`; 92 + const noMatchingEl = window.document.querySelector(selector); 93 + if (noMatchingEl) { 94 + if (list.visibleItems.length === 0) { 95 + noMatchingEl.classList.remove("d-none"); 96 + } else { 97 + if (!noMatchingEl.classList.contains("d-none")) { 98 + noMatchingEl.classList.add("d-none"); 99 + } 100 + } 101 + } 102 + } 103 + 104 + function setCategoryHash(category) { 105 + setHash({ category }); 106 + } 107 + 108 + function setPageHash(listingId, page) { 109 + const currentHash = getHash() || {}; 110 + currentHash[getListingPageKey(listingId)] = page; 111 + setHash(currentHash); 112 + } 113 + 114 + function getListingPageKey(listingId) { 115 + return `${listingId}-page`; 116 + } 117 + 118 + function refreshPaginationHandlers(listingId) { 119 + const listingEl = window.document.getElementById(listingId); 120 + const paginationEls = listingEl.querySelectorAll( 121 + ".pagination li.page-item:not(.disabled) .page.page-link" 122 + ); 123 + for (const paginationEl of paginationEls) { 124 + paginationEl.onclick = (sender) => { 125 + setPageHash(listingId, sender.target.getAttribute("data-i")); 126 + showPage(listingId, sender.target.getAttribute("data-i")); 127 + return false; 128 + }; 129 + } 130 + } 131 + 132 + function renderVisibleProgressiveImages(list) { 133 + // Run through the visible items and render any progressive images 134 + for (const item of list.visibleItems) { 135 + const itemEl = item.elm; 136 + if (itemEl) { 137 + const progressiveImgs = itemEl.querySelectorAll( 138 + `img[${kProgressiveAttr}]` 139 + ); 140 + for (const progressiveImg of progressiveImgs) { 141 + const srcValue = progressiveImg.getAttribute(kProgressiveAttr); 142 + if (srcValue) { 143 + progressiveImg.setAttribute("src", srcValue); 144 + } 145 + progressiveImg.removeAttribute(kProgressiveAttr); 146 + } 147 + } 148 + } 149 + } 150 + 151 + function getHash() { 152 + // Hashes are of the form 153 + // #name:value|name1:value1|name2:value2 154 + const currentUrl = new URL(window.location); 155 + const hashRaw = currentUrl.hash ? currentUrl.hash.slice(1) : undefined; 156 + return parseHash(hashRaw); 157 + } 158 + 159 + const kAnd = "&"; 160 + const kEquals = "="; 161 + 162 + function parseHash(hash) { 163 + if (!hash) { 164 + return undefined; 165 + } 166 + const hasValuesStrs = hash.split(kAnd); 167 + const hashValues = hasValuesStrs 168 + .map((hashValueStr) => { 169 + const vals = hashValueStr.split(kEquals); 170 + if (vals.length === 2) { 171 + return { name: vals[0], value: vals[1] }; 172 + } else { 173 + return undefined; 174 + } 175 + }) 176 + .filter((value) => { 177 + return value !== undefined; 178 + }); 179 + 180 + const hashObj = {}; 181 + hashValues.forEach((hashValue) => { 182 + hashObj[hashValue.name] = decodeURIComponent(hashValue.value); 183 + }); 184 + return hashObj; 185 + } 186 + 187 + function makeHash(obj) { 188 + return Object.keys(obj) 189 + .map((key) => { 190 + return `${key}${kEquals}${obj[key]}`; 191 + }) 192 + .join(kAnd); 193 + } 194 + 195 + function setHash(obj) { 196 + const hash = makeHash(obj); 197 + window.history.pushState(null, null, `#${hash}`); 198 + } 199 + 200 + function showPage(listingId, page) { 201 + const list = window["quarto-listings"][listingId]; 202 + if (list) { 203 + list.show((page - 1) * list.page + 1, list.page); 204 + } 205 + } 206 + 207 + function activateCategory(category) { 208 + // Deactivate existing categories 209 + const activeEls = window.document.querySelectorAll( 210 + ".quarto-listing-category .category.active" 211 + ); 212 + for (const activeEl of activeEls) { 213 + activeEl.classList.remove("active"); 214 + } 215 + 216 + // Activate this category 217 + const categoryEl = window.document.querySelector( 218 + `.quarto-listing-category .category[data-category='${btoa( 219 + encodeURIComponent(category) 220 + )}']` 221 + ); 222 + if (categoryEl) { 223 + categoryEl.classList.add("active"); 224 + } 225 + 226 + // Filter the listings to this category 227 + filterListingCategory(category); 228 + } 229 + 230 + function filterListingCategory(category) { 231 + const listingIds = Object.keys(window["quarto-listings"]); 232 + for (const listingId of listingIds) { 233 + const list = window["quarto-listings"][listingId]; 234 + if (list) { 235 + if (category === "") { 236 + // resets the filter 237 + list.filter(); 238 + } else { 239 + // filter to this category 240 + list.filter(function (item) { 241 + const itemValues = item.values(); 242 + if (itemValues.categories !== null) { 243 + const categories = decodeURIComponent( 244 + atob(itemValues.categories) 245 + ).split(","); 246 + return categories.includes(category); 247 + } else { 248 + return false; 249 + } 250 + }); 251 + } 252 + } 253 + } 254 + }
+45
_quarto.yml
··· 1 + project: 2 + type: website 3 + output-dir: _site 4 + render: 5 + - "*.qmd" 6 + website: 7 + description: "A personal website" 8 + site-url: https://rorylawless.com 9 + title: "Rory Lawless" 10 + repo-url: https://codeberg.org/RoryLawless/website/ 11 + page-footer: 12 + background: "#fbf5f5" 13 + foreground: "#070a0c" 14 + center: 15 + - text: "Made in DC by 👽" 16 + search: false 17 + navbar: 18 + background: "#24617a" 19 + foreground: "#fbf5f5" 20 + right: 21 + - icon: person 22 + text: "About" 23 + href: about.qmd 24 + aria-label: "About" 25 + - icon: bluesky 26 + href: https://bsky.app/profile/rorylawless.com 27 + aria-label: "Bluesky" 28 + - icon: linkedin 29 + href: https://www.linkedin.com/in/rory-lawless/ 30 + aria-label: "LinkedIn" 31 + - icon: git 32 + href: https://codeberg.org/RoryLawless/ 33 + aria-label: "Codeberg" 34 + format: 35 + html: 36 + theme: 37 + - none 38 + - assets/custom.scss 39 + highlight-style: a11y 40 + code-copy: true 41 + backgroundcolor: "#fbf5f5" 42 + mainfont: "Lato" 43 + fontcolor: "#070a0c" 44 + fontsize: 14pt 45 + linkcolor: "#183e4d"
+42
about.qmd
··· 1 + --- 2 + about: 3 + template: solana 4 + --- 5 + 6 + {{< include _about-short.qmd >}} 7 + 8 + ## Résumé 9 + 10 + ### Work 11 + **Data Analyst, Office of the Deputy Mayor for Education (DME)** 12 + 13 + *Washington, DC.* 2022–Present 14 + 15 + - Contributed analysis for key publications, including 2023 Master Facilities Plan. 16 + - Improved school enrollment projections processes, leading cross-agency collaboration. 17 + - Information officer and racial justice & equity team member. 18 + 19 + **Associate Data Analyst, Financial Conduct Authority (FCA)** 20 + 21 + *London, UK.* 2020–2022 22 + 23 + - Produced insightful and impactful analysis for internal and external audiences in support of high profile, politically sensitive publications. 24 + - Data collection and extraction using SQL and web scraping techniques. 25 + - Developed best practice for data management and replicable analysis through training and providing ad hoc advice and support. 26 + 27 + **Research Assistant - Data, Child Outcomes Research Consortium (CORC)** 28 + 29 + *London, UK.* 2018–2020 30 + 31 + - Supporting services to submit data to the organization, including one-to-one support as well as organizing and hosting webinars. 32 + - Redesigned data collection tools to improve quality of submissions and automated internal data validation processes. 33 + 34 + ### Education 35 + 36 + **MSc Democracy and Comparative Politics, Distinction** 37 + 38 + *University College London. London, UK.* 2016 39 + 40 + **BA (Hons.) Politics, Upper Second Class** 41 + 42 + *Royal Holloway, University of London. Surrey, UK.* 2013
+40
assets/custom.scss
··· 1 + /*-- scss:defaults --*/ 2 + 3 + 4 + /*-- scss:rules --*/ 5 + 6 + $web-font-path: "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap" !default; 7 + 8 + @if $web-font-path { 9 + @import url($web-font-path); 10 + } 11 + 12 + .navbar-container { 13 + width: 820px !important; 14 + } 15 + 16 + .navbar-title { 17 + font-family: 'Playfair Display', serif; 18 + font-size: 32pt; 19 + } 20 + 21 + .quarto-listing-table.no-borders, 22 + .quarto-listing-table.no-borders tbody, 23 + .quarto-listing-table.no-borders tr, 24 + .quarto-listing-table.no-borders td { 25 + border: none !important; 26 + } 27 + 28 + .quarto-listing-table.no-borders td:nth-child(2) { 29 + text-align: right; 30 + } 31 + 32 + h2 { 33 + border-bottom: 0; 34 + padding-bottom: 0; 35 + margin-top: 1rem; 36 + } 37 + 38 + .table> :not(caption)>*>* { 39 + padding: 0; 40 + }
+24
assets/template.ejs
··· 1 + <% const fields=['title', 'date' ]; %> 2 + 3 + ```{=html} 4 + 5 + <table class="quarto-listing-table table no-borders"> 6 + <tbody class="list"> 7 + <% for (const item of items) { %> 8 + <tr> 9 + <% for (const field of fields) { %> 10 + <td> 11 + <% if (field==='title' && (item.outputHref || item.path)) { %> 12 + <a href="<%= item.outputHref || item.path %>"> 13 + <%= item[field] %> 14 + </a> 15 + <% } else { %> 16 + <%= item[field] %> 17 + <% } %> 18 + </td> 19 + <% } %> 20 + </tr> 21 + <% } %> 22 + </tbody> 23 + </table> 24 + ```
+19
index.qmd
··· 1 + --- 2 + listing: 3 + id: index-listing 4 + contents: posts 5 + template: assets/template.ejs 6 + date-format: "iso" 7 + sort: "date desc" 8 + categories: false 9 + sort-ui: false 10 + filter-ui: false 11 + feed: true 12 + --- 13 + 14 + ## Posts 15 + 16 + ::: {#index-listing} 17 + ::: 18 + 19 + {{< include _about-short.qmd >}}
+6
posts/_metadata.yml
··· 1 + # options specified here will apply to all posts in this folder 2 + 3 + # freeze computational output 4 + # (see https://quarto.org/docs/projects/code-execution.html#freeze) 5 + freeze: auto 6 + date-format: iso
+77
posts/the-basics-of-duckdb-in-r/index.qmd
··· 1 + --- 2 + title: "The basics of DuckDB in R" 3 + author: Rory Lawless 4 + date: 2025-03-30 5 + lastmod: 2026-01-01 6 + format: html 7 + aliases: 8 + - r-duckdb-and-me.html 9 + --- 10 + 11 + Over the past year, [DuckDB](https://duckdb.org/docs/stable/clients/r) has gradually become an important part of my data science workflow - at first clumsily, then seamlessly. I don’t typically work with large datasets, however, integrating DuckDB has addressed some of my frustrations, especially when dealing with hardware limitations and moderately-sized but inefficiently stored data. With this in mind, here are two major benefits I’ve found since integrating DuckDB into my workflow. 12 + 13 + ## Handling larger-than-memory data 14 + 15 + As noted, I don't work with very large data often but I still run into annoying issues caused by repeated reloading of data after making mistakes. Now, this is not an issue for a .csv file containing a few hundred rows and, for larger files or those stored in legacy formats, I could add a "backup" step to my code, like so: 16 + 17 + ```{r} 18 + #| eval: false 19 + 20 + data <- read.csv("some-data-file.csv") 21 + data_backup <- data 22 + 23 + # Do some work on data, maybe make a mistake... 24 + 25 + data <- data_backup 26 + 27 + ``` 28 + 29 + This works fine, but I consider it an anti-pattern and ought to, in my opinion, be avoided. Instead of adding this extra step - likely increasing the memory used in the R session - you can use DuckDB to directly query files stored on disk, without having to load them into memory first. 30 + 31 + ```{r} 32 + #| eval: false 33 + 34 + library(tidyverse) 35 + library(duckdb) 36 + 37 + # Create a DuckDB connection 38 + con <- dbConnect(duckdb::duckdb()) 39 + 40 + # Write a SQL query to read data directly from the CSV file 41 + data <- dbGetQuery( 42 + con, 43 + "SELECT col_1, col_2, col_4, col_10 44 + FROM 'some-data-file.csv' 45 + WHERE col_10 = 'some_value'" 46 + ) 47 + ``` 48 + 49 + This may seem more complicated at first, and does require some knowledge of SQL, but it is a very efficient way of working with larger datasets, especially in the early stages when you're still exploring the data and working out what you're going to do with it. 50 + 51 + ## {duckplyr} 52 + 53 + A game-changer for me, which really accelerated my adoption of DuckDB as a backend for processing data, was the [{duckplyr}](https://duckplyr.tidyverse.org) package. Those familiar with [{dbplyr}](https://dbplyr.tidyverse.org) will understand the theory behind this package; it allows queries to be built using the standard set of [{dplyr}](https://dplyr.tidyverse.org) functions, which are converted to SQL behind the scenes. 54 + 55 + 56 + ```{r} 57 + #| eval: false 58 + 59 + library(tidyverse) 60 + library(duckplyr) 61 + 62 + # Read CSV using DuckDB behind the scenes 63 + data <- read_csv_duckdb("some-data-file.csv") 64 + 65 + # Perform data manipulation using dplyr syntax 66 + data <- data |> 67 + select(col_1, col_2, col_4, col_10) |> 68 + filter(col_10 == "some_value") 69 + ``` 70 + 71 + Aside from the `read_csv_duckdb()` function, the rest of the code will be familiar to anyone who has used {dplyr} before. The main advantage of using {duckplyr} over writing SQL and using the [{DBI}](https://dbi.r-dbi.org) package is readability - using common {dplyr} functions makes it accessible to a wider range of users. This is a big benefit for teams where not everyone is comfortable reading or writing SQL. 72 + 73 + Additionally, should the original author fall off the face of the earth, the code is still maintainable by others and readily adapted to eliminate the dependency on DuckDB. 74 + 75 + ## Final thoughts 76 + 77 + DuckDB and R are a great combination, allowing me to overcome some of my (self-inflicted?) frustrations in my day-to-day data work. With {duckplyr}, querying data directly from files has smoothed out some of the rough edges in my workflow.