-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.html
513 lines (483 loc) · 29.9 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head>
<meta charset="utf-8">
<meta name="generator" content="quarto-1.3.433">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<meta name="author" content="S. Andrews">
<title>pubcomm</title>
<style>
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
div.columns{display: flex; gap: min(4vw, 1.5em);}
div.column{flex: auto; overflow-x: auto;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
ul.task-list{list-style: none;}
ul.task-list li input[type="checkbox"] {
width: 0.8em;
margin: 0 0.8em 0.2em -1em; /* quarto-specific, see https://github.com/quarto-dev/quarto-cli/issues/4556 */
vertical-align: middle;
}
</style>
<script src="index_files/libs/clipboard/clipboard.min.js"></script>
<script src="index_files/libs/quarto-html/quarto.js"></script>
<script src="index_files/libs/quarto-html/popper.min.js"></script>
<script src="index_files/libs/quarto-html/tippy.umd.min.js"></script>
<script src="index_files/libs/quarto-html/anchor.min.js"></script>
<link href="index_files/libs/quarto-html/tippy.css" rel="stylesheet">
<link href="index_files/libs/quarto-html/quarto-syntax-highlighting.css" rel="stylesheet" id="quarto-text-highlighting-styles">
<script src="index_files/libs/bootstrap/bootstrap.min.js"></script>
<link href="index_files/libs/bootstrap/bootstrap-icons.css" rel="stylesheet">
<link href="index_files/libs/bootstrap/bootstrap.min.css" rel="stylesheet" id="quarto-bootstrap" data-mode="light">
</head>
<body>
<div id="quarto-content" class="page-columns page-rows-contents page-layout-article">
<div id="quarto-margin-sidebar" class="sidebar margin-sidebar">
<nav id="TOC" role="doc-toc" class="toc-active">
<h2 id="toc-title">about this project</h2>
<ul>
<li><a href="#description-and-process-summary" id="toc-description-and-process-summary" class="nav-link active" data-scroll-target="#description-and-process-summary">Description and Process Summary</a></li>
<li><a href="#context" id="toc-context" class="nav-link" data-scroll-target="#context">Context</a>
<ul class="collapse">
<li><a href="#about-city-council-meetings-and-public-comment" id="toc-about-city-council-meetings-and-public-comment" class="nav-link" data-scroll-target="#about-city-council-meetings-and-public-comment">About City Council Meetings and Public Comment</a></li>
<li><a href="#about-written-communications" id="toc-about-written-communications" class="nav-link" data-scroll-target="#about-written-communications">About Written Communications</a></li>
</ul></li>
<li><a href="#data-collection" id="toc-data-collection" class="nav-link" data-scroll-target="#data-collection">Data Collection</a>
<ul class="collapse">
<li><a href="#meeting-agendas" id="toc-meeting-agendas" class="nav-link" data-scroll-target="#meeting-agendas"><span id="agendas">Meeting Agendas</span></a></li>
<li><a href="#supplemental-communications-packets" id="toc-supplemental-communications-packets" class="nav-link" data-scroll-target="#supplemental-communications-packets"><span id="publicrecords">Supplemental Communications Packets</span></a></li>
</ul></li>
<li><a href="#data-analysis" id="toc-data-analysis" class="nav-link" data-scroll-target="#data-analysis">Data Analysis</a>
<ul class="collapse">
<li><a href="#parsing-the-agenda" id="toc-parsing-the-agenda" class="nav-link" data-scroll-target="#parsing-the-agenda">Parsing the Agenda</a></li>
<li><a href="#parsing-written-communications-packets" id="toc-parsing-written-communications-packets" class="nav-link" data-scroll-target="#parsing-written-communications-packets">Parsing Written Communications Packets</a></li>
<li><a href="#synthesizing-content-of-written-communications-packets" id="toc-synthesizing-content-of-written-communications-packets" class="nav-link" data-scroll-target="#synthesizing-content-of-written-communications-packets">Synthesizing Content of Written Communications Packets</a></li>
</ul></li>
<li><a href="#exceptions" id="toc-exceptions" class="nav-link" data-scroll-target="#exceptions">Exceptions</a></li>
<li><a href="#remaining-work" id="toc-remaining-work" class="nav-link" data-scroll-target="#remaining-work">Remaining Work</a>
<ul class="collapse">
<li><a href="#blocking-issues" id="toc-blocking-issues" class="nav-link" data-scroll-target="#blocking-issues">Blocking Issues</a></li>
<li><a href="#validation" id="toc-validation" class="nav-link" data-scroll-target="#validation">Validation</a></li>
<li><a href="#automation" id="toc-automation" class="nav-link" data-scroll-target="#automation">Automation</a></li>
<li><a href="#and-then" id="toc-and-then" class="nav-link" data-scroll-target="#and-then">And then…</a></li>
</ul></li>
<li><a href="#misc-notes" id="toc-misc-notes" class="nav-link" data-scroll-target="#misc-notes">Misc Notes</a>
<ul class="collapse">
<li><a href="#on-intentions" id="toc-on-intentions" class="nav-link" data-scroll-target="#on-intentions">On Intentions</a></li>
<li><a href="#on-data-sources" id="toc-on-data-sources" class="nav-link" data-scroll-target="#on-data-sources">On Data Sources</a></li>
</ul></li>
<li><a href="#project-metadata" id="toc-project-metadata" class="nav-link" data-scroll-target="#project-metadata">Project Metadata</a></li>
</ul>
</nav>
</div>
<main class="content" id="quarto-document-content">
<header id="title-block-header" class="quarto-title-block default">
<div class="quarto-title">
<h1 class="title">pubcomm</h1>
</div>
<div class="quarto-title-meta">
<div>
<div class="quarto-title-meta-heading">Author</div>
<div class="quarto-title-meta-contents">
<p>S. Andrews </p>
</div>
</div>
</div>
</header>
<section id="description-and-process-summary" class="level1">
<h1>Description and Process Summary</h1>
<p><a href="https://github.com/jellomoat/pubcomm">This project</a> uses AI to summarize and code letters to city council from the public to provide an aggregated view of public sentiment on local issues.</p>
<p>This project was designed to make it easier for journalists to understand public sentiment as expressed in letters written to a city government. As local issues can garner tens or hundreds of letters, and local journalism is seldom well-resourced, automation could be key in delivering timely and accurate reporting to the public.</p>
<!-- This project takes a raw corpus of scanned emails and letters (PDF), extracts the text from it to create a parseable dataset, generates a pithy summary for each letter, and distills a label for whether the letter seemed to express support for or opposition to a policy recommendation put forth by the city council as specified in the meeting agenda.
-->
<p>In this project, PDF letters and emails are (1) turned into text, (2) summarized with AI, and (3) coded for whether they express support or opposition to an issue, also with AI.</p>
<p><img src="./doc_images/cap_process.png" alt="process summary" width="100%"></p>
</section>
<section id="context" class="level1">
<h1>Context</h1>
<section id="about-city-council-meetings-and-public-comment" class="level2">
<h2 class="anchored" data-anchor-id="about-city-council-meetings-and-public-comment">About City Council Meetings and Public Comment</h2>
<p>The Berkeley City Council meets at least twice a month. These meetings are typically five hours long.</p>
<p>The City Council invites members of the public to comment for up to 2 minutes at the beginning of a meeting, at the end of a meeting, and after agenda action items are called. There are many community members who show up to express their position on council agenda items, some staying nearly the entire five hours before providing public comment.</p>
<p><img src="./doc_images/cap_recent_meeting.png" width="35%" style="margin: 2%" alt="screencap from recent meeting showing 2 min timer" border="1px"></p>
<!-- Their participation is typically noted collectively and succinctly as: "Action/Public Comment/Public Testimony: < # > speakers."
For example, community members provided public comment 87 times at the meeting held on July 7th, 2023.
The content of the first 58 were documented as:
* "Public Comment on Non-Agenda Matters: 9 speakers."
* "Public Comment on Consent Calendar and Information Items Only: 49 speakers."
<img src="./cap_annotatedagenda_pubcomm_example.png" width="35%" style="margin: 2%" alt="screencap of annotated agenda" border="1px"></img> -->
<!-- Some people stay the entire meeting waiting to comment on an issue only for that agenda item to be bumped to the following meeting. There are also community members who attend hoping to speak but need to leave before their item is called. They are often youth and parents of young children who are unable to stay all five hours on a school night.
At a recent meeting held on July 7th, 2023, community members provided live public comment 87 times. There were both agenda items bumped and community members who left before having a chance to provide public comment.
-->
<p>There are also many who would be civically engaged but are unable to attend or who can only attend for part of the meeting. These meetings are held on Tuesday evenings – school nights – making it all the more challenging for youth and parents of young children to participate.</p>
<p>For example, at a recent July meeting community members provided live public comment 87 times. At the end of the meeting a councilmember stated that there were youth in attendance who she knew wanted to speak on an issue but needed to leave before their agenda item was called.</p>
</section>
<section id="about-written-communications" class="level2">
<h2 class="anchored" data-anchor-id="about-written-communications">About Written Communications</h2>
<p>The City Council acknowledges that the length and format of public meetings present barriers to civic engagement. It even periodically convenes meetings to discuss specifically how to mitigate those barriers.</p>
<p>One alternative to live attendance: public comment in writing, emailed in advance to the city clerk for the council’s consideration and inclusion in the public record.</p>
<p><img src="./doc_images/cap_commentinwriting.png" width="65%" style="margin: 2%" alt="screencap of comment in writing section of berkeley's 'participating in city council meetings' page" border="1px"></p>
<p>At this same mid-July meeting, 196 people emailed to share their support for or opposition to upcoming agenda items. These emails cumulatively span 336 pages of PDF.</p>
<p><img src="./doc_images/inspo_pt3.png" width="65%" style="margin: 2%" alt="" border="1px"></p>
<p>The content of these written communications is presented en masse in the agenda as an enumerated list of people and item numbers. There is no additional context beyond a note indicating that the raw emails are available in the city’s Public Records Online database.</p>
<p><img src="./doc_images/cap_agendacomments.png" width="65%" style="margin: 2%" alt="screencap of agenda comments"></p>
<p>In a city where there is significant civic engagement, how can council members and the public at large actually synthesize such a massive quantity of qualitative data?</p>
</section>
</section>
<section id="data-collection" class="level1">
<h1>Data Collection</h1>
<p>Data for this project was collected from Berkeley’s <a href="#agendas">City Council Agendas</a> and Berkeley’s <a href="#publicrecords">Public Records Online Database</a>, more detail below.</p>
<section id="meeting-agendas" class="level2">
<h2 class="anchored" data-anchor-id="meeting-agendas"><span id="agendas">Meeting Agendas</span></h2>
<p>Data Source: Berkeley <a href="https://berkeleyca.gov/your-government/city-council/city-council-agendas">City Council Agendas</a> page</p>
<p>Agendas are posted online to <a href="https://berkeleyca.gov/your-government/city-council/city-council-agendas">this page</a> before each meeting, then again with annotations and audiovisual recordings following the meeting.</p>
<p><img src="./doc_images/cap_cobagendas.png" width="35%" style="margin: 2%" alt="screencap of agendas page" border="1px"> <img src="./doc_images/cap_annotatedagenda.png" width="35%" style="margin: 2%" alt="screencap of annotated agenda" border="1px"></p>
</section>
<section id="supplemental-communications-packets" class="level2">
<h2 class="anchored" data-anchor-id="supplemental-communications-packets"><span id="publicrecords">Supplemental Communications Packets</span></h2>
<p>Data Source: Berkeley’s <a href="https://records.cityofberkeley.info/PublicAccess/paFiles/cqFiles/index.html">Public Records Online Database</a></p>
<p>Written communications are available for download in the Public Records Online Database. These take the form of PDF scans of emails that were previously printed and assembled in physical packets. These supplemental packets commonly contain hundreds of pages and hundreds of constituent comments per meeting.</p>
<p><img src="./doc_images/cap_records.png" width="85%" style="margin: 2%" alt="screencap of records search" border="1px"></p>
</section>
</section>
<section id="data-analysis" class="level1">
<h1>Data Analysis</h1>
<section id="parsing-the-agenda" class="level2">
<h2 class="anchored" data-anchor-id="parsing-the-agenda">Parsing the Agenda</h2>
<p>I first used beautifulsoup to scrape and parse meeting agendas posted online. Aside from some tricky regex this was not too bad.</p>
<p><img src="./doc_images/process_pt1.png" width="85%" style="margin: 2%" alt="" border="1px"></p>
<p>Here’s what the output of that looks like, which I loaded into a dataframe and wrote down to a csv file.</p>
<p><img src="./doc_images/process_pt2.png" width="85%" style="margin: 2%" alt="" border="1px"></p>
</section>
<section id="parsing-written-communications-packets" class="level2">
<h2 class="anchored" data-anchor-id="parsing-written-communications-packets">Parsing Written Communications Packets</h2>
<p>I then parsed Written Communications PDFs to get an index of letter-writers and their associated correspondence IDs. I tried extracting text with pdfplumber then landed on using an OCR library called pytesseract.</p>
<p>I also used pytesseract to parse content, first converting the PDF into images, then converting the images into text.</p>
<p><img src="./doc_images/process_pt3.png" width="85%" style="margin: 2%" alt="" border="1px"></p>
<p>I pulled all this together in a dataframe containing the newly extracted text.</p>
<p><img src="./doc_images/process_pt4.png" width="85%" style="margin: 2%" alt="" border="1px"></p>
</section>
<section id="synthesizing-content-of-written-communications-packets" class="level2">
<h2 class="anchored" data-anchor-id="synthesizing-content-of-written-communications-packets">Synthesizing Content of Written Communications Packets</h2>
<p>Now that I had these documents in a more friendly format, my next step was to figure out how to efficiently analyze their contents. I tried using several <a href="https://spacy.io">natural</a> <a href="https://www.nltk.org/">language</a> <a href="https://scikit-learn.org/stable/">processing</a> libraries. After a couple days fumbling through NLP docs and <a href="https://investigate.ai/text-analysis/introduction-to-topic-modeling/">investigate.ai’s text analysis guides</a>, I eventually consulted Soma, who suggested using ChatGPT. Soma also helped outline a possible approach.</p>
<p>I then went about designing prompts using the ChatGPT interface online. This is a screenshot of one of the first among many prompts I tested.</p>
<p><img src="./doc_images/process_pt5.png" width="85%" style="margin: 2%" alt="" border="1px"></p>
<p>This is the prompt I moved forward with:</p>
<p>“For this single email, provide a one-sentence summary and categorize their position (support, oppose, other) on the provided recommendations.”</p>
<p>I also specified the input and output format.</p>
<p>I fed that prompt to the ChatOpenAI API, then parsed and stored the results in a dataframe and eventually a csv. <img src="./doc_images/process_pt6.png" width="85%" style="margin: 2%" alt="" border="1px"></p>
<p>I then aggregated the likely positions of those who submitted letters to the council and grouped them by agenda item number.</p>
<p>The final outputs of this process were CSVs and dataframes containing:</p>
<ul>
<li>raw text extracted from written communications packets</li>
<li>one-sentence summaries of the text in relation to policy recommendations</li>
<li>predicted constituent sentiments (support, oppose, other)</li>
<li>aggregated positions by agenda item</li>
</ul>
<p><img src="./doc_images/process_pt7.png" width="85%" style="margin: 2%" alt="" border="1px"></p>
</section>
</section>
<section id="exceptions" class="level1">
<h1>Exceptions</h1>
<p>I was excited to have something that seemed to work. I skimmed through packets and manually spot-checked results, reviewing communications alongside AI-generated summaries and dispositions.</p>
<p>Everything seemed great and then… a petition appeared! A 25-page document of signatures in support of an agenda item on daylighting Strawberry Creek.</p>
<p><img src="./doc_images/cap_petitions.png" alt="screencap of petitions page" width="50%" border="1px"></p>
<p>It was an anomaly embedded deep in the third supplemental communications packet, beginning on page 94 of 131. Communications ID #192 with email subject: “Strawberry Creek Paper Petition Responses 1-247.pdf”. This petition was submitted as an attachment to an emailed letter.</p>
<p><img src="./doc_images/cap_petitions_pt2.png" alt="screencap of petitions email" width="50%" border="1px"></p>
<p>While this system clearly identified that ID #192 expresses support for this agenda item, it erroneously distilled a letter and all its attachments into a single tally for its corresponding agenda item.</p>
<p>Upon closer examination I discovered there were several (!!) petitions in this packet, so this was not an isolated issue and would need to be addressed.</p>
</section>
<section id="remaining-work" class="level1">
<h1>Remaining Work</h1>
<section id="blocking-issues" class="level2">
<h2 class="anchored" data-anchor-id="blocking-issues">Blocking Issues</h2>
<p>This project is not yet ready for release. Here are identified blocking issues:</p>
<ul>
<li>Flags needed for appended file attachments</li>
<li>Unexpected data formats like signature petitions</li>
</ul>
</section>
<section id="validation" class="level2">
<h2 class="anchored" data-anchor-id="validation">Validation</h2>
<p>I’ve spot-checked the first test-batch of results and found no issues beyond the ones specified above. That said, much more testing is needed, outlined further below.</p>
<ul>
<li>Test with data from more meetings</li>
<li>Hand-tag more data to compare generated results against a ground truth</li>
<li>Benchmark against a few other approaches for position predictions, eg bag of words, using a simple regex pattern, and using smaller language models</li>
</ul>
</section>
<section id="automation" class="level2">
<h2 class="anchored" data-anchor-id="automation">Automation</h2>
<p>This project would be a lot more usable if these parts could be automated:</p>
<ul>
<li>Pulling down raw documents, ie agendas, written communications</li>
<li>Parsing and sharing results</li>
</ul>
</section>
<section id="and-then" class="level2">
<h2 class="anchored" data-anchor-id="and-then">And then…</h2>
<p>After everything above is addressed, I hope to:</p>
<ul>
<li>Use it to write a story :)</li>
<li>Share it with people!</li>
<li>Generalize and adapt for other municipalities</li>
<li>Also analyze audio data from recordings</li>
</ul>
</section>
</section>
<section id="misc-notes" class="level1">
<h1>Misc Notes</h1>
<section id="on-intentions" class="level2">
<h2 class="anchored" data-anchor-id="on-intentions">On Intentions</h2>
<p>This project is <em>not</em> meant to be a replacement for actually reading the letters in their entirety. There is still considerable value in reviewing the letters themselves. It merely seeks to augment the letters, to help both the public and council members get a broader perspective of each comment in context.</p>
</section>
<section id="on-data-sources" class="level2">
<h2 class="anchored" data-anchor-id="on-data-sources">On Data Sources</h2>
<p>Since all original documents are part of the public record I left identifiable text as is. But please <a href="mailto: [email protected]">contact me</a> if you see your name/email address in an image and would like your info scrubbed out.</p>
<p>Also <a href="https://www.jellomoat.com/contact">please reach out</a> if you have questions, would like to chat about the project, or just want to say hello :)</p>
</section>
</section>
<section id="project-metadata" class="level1">
<h1>Project Metadata</h1>
<p>This was initially created as my fourth and final project in the <a href="https://ledeprogram.com/">Lede Program</a>. Many thanks to these kind folks who helped shape the project: <a href="https://github.com/jsoma">Soma</a>, <a href="https://github.com/yinleon">Leon</a>, and <a href="https://hypergradient.ai/">Will</a> ✨</p>
<p>This site was adapted from a presentation given on August 11, 2023. I probably will replace this with a proper docs page if this ever gets off the ground enough for distro.</p>
<p>Slides here => <a href="https://bit.ly/pubcomm-public">https://bit.ly/pubcomm-public</a></p>
<p>Repo with code and raw docs here => <a href="https://github.com/jellomoat/pubcomm">https://github.com/jellomoat/pubcomm</a></p>
<p>(Lastly, “pubcomm” is just a working name - please lmk if you can think of something better!)</p>
</section>
</main>
<!-- /main column -->
<script id="quarto-html-after-body" type="application/javascript">
window.document.addEventListener("DOMContentLoaded", function (event) {
const toggleBodyColorMode = (bsSheetEl) => {
const mode = bsSheetEl.getAttribute("data-mode");
const bodyEl = window.document.querySelector("body");
if (mode === "dark") {
bodyEl.classList.add("quarto-dark");
bodyEl.classList.remove("quarto-light");
} else {
bodyEl.classList.add("quarto-light");
bodyEl.classList.remove("quarto-dark");
}
}
const toggleBodyColorPrimary = () => {
const bsSheetEl = window.document.querySelector("link#quarto-bootstrap");
if (bsSheetEl) {
toggleBodyColorMode(bsSheetEl);
}
}
toggleBodyColorPrimary();
const icon = "";
const anchorJS = new window.AnchorJS();
anchorJS.options = {
placement: 'right',
icon: icon
};
anchorJS.add('.anchored');
const isCodeAnnotation = (el) => {
for (const clz of el.classList) {
if (clz.startsWith('code-annotation-')) {
return true;
}
}
return false;
}
const clipboard = new window.ClipboardJS('.code-copy-button', {
text: function(trigger) {
const codeEl = trigger.previousElementSibling.cloneNode(true);
for (const childEl of codeEl.children) {
if (isCodeAnnotation(childEl)) {
childEl.remove();
}
}
return codeEl.innerText;
}
});
clipboard.on('success', function(e) {
// button target
const button = e.trigger;
// don't keep focus
button.blur();
// flash "checked"
button.classList.add('code-copy-button-checked');
var currentTitle = button.getAttribute("title");
button.setAttribute("title", "Copied!");
let tooltip;
if (window.bootstrap) {
button.setAttribute("data-bs-toggle", "tooltip");
button.setAttribute("data-bs-placement", "left");
button.setAttribute("data-bs-title", "Copied!");
tooltip = new bootstrap.Tooltip(button,
{ trigger: "manual",
customClass: "code-copy-button-tooltip",
offset: [0, -8]});
tooltip.show();
}
setTimeout(function() {
if (tooltip) {
tooltip.hide();
button.removeAttribute("data-bs-title");
button.removeAttribute("data-bs-toggle");
button.removeAttribute("data-bs-placement");
}
button.setAttribute("title", currentTitle);
button.classList.remove('code-copy-button-checked');
}, 1000);
// clear code selection
e.clearSelection();
});
function tippyHover(el, contentFn) {
const config = {
allowHTML: true,
content: contentFn,
maxWidth: 500,
delay: 100,
arrow: false,
appendTo: function(el) {
return el.parentElement;
},
interactive: true,
interactiveBorder: 10,
theme: 'quarto',
placement: 'bottom-start'
};
window.tippy(el, config);
}
const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]');
for (var i=0; i<noterefs.length; i++) {
const ref = noterefs[i];
tippyHover(ref, function() {
// use id or data attribute instead here
let href = ref.getAttribute('data-footnote-href') || ref.getAttribute('href');
try { href = new URL(href).hash; } catch {}
const id = href.replace(/^#\/?/, "");
const note = window.document.getElementById(id);
return note.innerHTML;
});
}
let selectedAnnoteEl;
const selectorForAnnotation = ( cell, annotation) => {
let cellAttr = 'data-code-cell="' + cell + '"';
let lineAttr = 'data-code-annotation="' + annotation + '"';
const selector = 'span[' + cellAttr + '][' + lineAttr + ']';
return selector;
}
const selectCodeLines = (annoteEl) => {
const doc = window.document;
const targetCell = annoteEl.getAttribute("data-target-cell");
const targetAnnotation = annoteEl.getAttribute("data-target-annotation");
const annoteSpan = window.document.querySelector(selectorForAnnotation(targetCell, targetAnnotation));
const lines = annoteSpan.getAttribute("data-code-lines").split(",");
const lineIds = lines.map((line) => {
return targetCell + "-" + line;
})
let top = null;
let height = null;
let parent = null;
if (lineIds.length > 0) {
//compute the position of the single el (top and bottom and make a div)
const el = window.document.getElementById(lineIds[0]);
top = el.offsetTop;
height = el.offsetHeight;
parent = el.parentElement.parentElement;
if (lineIds.length > 1) {
const lastEl = window.document.getElementById(lineIds[lineIds.length - 1]);
const bottom = lastEl.offsetTop + lastEl.offsetHeight;
height = bottom - top;
}
if (top !== null && height !== null && parent !== null) {
// cook up a div (if necessary) and position it
let div = window.document.getElementById("code-annotation-line-highlight");
if (div === null) {
div = window.document.createElement("div");
div.setAttribute("id", "code-annotation-line-highlight");
div.style.position = 'absolute';
parent.appendChild(div);
}
div.style.top = top - 2 + "px";
div.style.height = height + 4 + "px";
let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter");
if (gutterDiv === null) {
gutterDiv = window.document.createElement("div");
gutterDiv.setAttribute("id", "code-annotation-line-highlight-gutter");
gutterDiv.style.position = 'absolute';
const codeCell = window.document.getElementById(targetCell);
const gutter = codeCell.querySelector('.code-annotation-gutter');
gutter.appendChild(gutterDiv);
}
gutterDiv.style.top = top - 2 + "px";
gutterDiv.style.height = height + 4 + "px";
}
selectedAnnoteEl = annoteEl;
}
};
const unselectCodeLines = () => {
const elementsIds = ["code-annotation-line-highlight", "code-annotation-line-highlight-gutter"];
elementsIds.forEach((elId) => {
const div = window.document.getElementById(elId);
if (div) {
div.remove();
}
});
selectedAnnoteEl = undefined;
};
// Attach click handler to the DT
const annoteDls = window.document.querySelectorAll('dt[data-target-cell]');
for (const annoteDlNode of annoteDls) {
annoteDlNode.addEventListener('click', (event) => {
const clickedEl = event.target;
if (clickedEl !== selectedAnnoteEl) {
unselectCodeLines();
const activeEl = window.document.querySelector('dt[data-target-cell].code-annotation-active');
if (activeEl) {
activeEl.classList.remove('code-annotation-active');
}
selectCodeLines(clickedEl);
clickedEl.classList.add('code-annotation-active');
} else {
// Unselect the line
unselectCodeLines();
clickedEl.classList.remove('code-annotation-active');
}
});
}
const findCites = (el) => {
const parentEl = el.parentElement;
if (parentEl) {
const cites = parentEl.dataset.cites;
if (cites) {
return {
el,
cites: cites.split(' ')
};
} else {
return findCites(el.parentElement)
}
} else {
return undefined;
}
};
var bibliorefs = window.document.querySelectorAll('a[role="doc-biblioref"]');
for (var i=0; i<bibliorefs.length; i++) {
const ref = bibliorefs[i];
const citeInfo = findCites(ref);
if (citeInfo) {
tippyHover(citeInfo.el, function() {
var popup = window.document.createElement('div');
citeInfo.cites.forEach(function(cite) {
var citeDiv = window.document.createElement('div');
citeDiv.classList.add('hanging-indent');
citeDiv.classList.add('csl-entry');
var biblioDiv = window.document.getElementById('ref-' + cite);
if (biblioDiv) {
citeDiv.innerHTML = biblioDiv.innerHTML;
}
popup.appendChild(citeDiv);
});
return popup.innerHTML;
});
}
}
});
</script>
</div> <!-- /content -->
</body></html>