This is a valid Atom 1.0 feed.
This feed is valid, but interoperability with the widest range of feed readers could be improved by implementing the following recommendations.
line 25, column 0: (6 occurrences) [help]
<p>Way back in March of this year, I took a lo ...
line 71, column 0: (7 occurrences) [help]
<p>As I continue to play with, and really frea ...
line 71, column 0: (7 occurrences) [help]
<p>As I continue to play with, and really frea ...
line 71, column 0: (7 occurrences) [help]
<p>As I continue to play with, and really frea ...
line 71, column 0: (7 occurrences) [help]
<p>As I continue to play with, and really frea ...
line 71, column 0: (7 occurrences) [help]
<p>As I continue to play with, and really frea ...
line 71, column 0: (21 occurrences) [help]
<p>As I continue to play with, and really frea ...
line 71, column 0: (10 occurrences) [help]
<p>As I continue to play with, and really frea ...
line 95, column 0: (3 occurrences) [help]
<p>For my American readers, I hope you are abl ...
line 95, column 0: (3 occurrences) [help]
<p>For my American readers, I hope you are abl ...
line 167, column 0: (4 occurrences) [help]
<p>If you missed my <a href="https://c ...
line 167, column 0: (4 occurrences) [help]
<p>If you missed my <a href="https://c ...
<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Raymond Camden</title>
<link href="https://www.raymondcamden.com/feed.xml" rel="self" type="application/atom+xml"></link>
<link href="https://www.raymondcamden.com/" rel="alternate" type="text/html"></link>
<subtitle>Father, husband, developer relations and web standards expert, and cat demo builder.</subtitle>
<updated>2024-09-05T21:47:42+00:00</updated>
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
<id>https://www.raymondcamden.com/feed.xml</id>
<generator>Eleventy</generator>
<entry>
<id>https://www.raymondcamden.com/2024/09/05/using-pdf-content-with-google-gemini-an-update</id>
<title>Using PDF Content with Google Gemini - An Update</title>
<updated>2024-09-05T18:00:00+00:00</updated>
<link href="https://www.raymondcamden.com/2024/09/05/using-pdf-content-with-google-gemini-an-update" rel="alternate" type="text/html" title="Using PDF Content with Google Gemini - An Update"/>
<content type="html">
<p>Way back in March of this year, I took a look at using Google's Gemini APIs to analyze PDF documents (<a href="https://www.raymondcamden.com/2024/03/22/using-pdf-content-with-google-gemini">&quot;Using PDF Content with Google Gemini&quot;</a>). At the time, the Gemini API didn't support PDF documents, so I made use of our (Adobe) <a href="https://developer.adobe.com/document-services/apis/pdf-extract/">PDF Extract</a> service to get the text content out from the document. This &quot;worked&quot; but was possibly less than ideal as my &quot;glom all the text together&quot; approach didn't really represent the PDF well. The PDF Extract API returns information about text context (like if it is a header for example), but my method ignored that. I'm happy to share that Gemini now supports PDF files natively. Let's take a look at how this works.</p><h2 id="uploading-pdfs" tabindex="-1"><a class="header-anchor" href="#uploading-pdfs">Uploading PDFs</a></h2><p>To begin, you need to provide your PDF to Gemini. This is done via the Files API. I <a href="https://www.raymondcamden.com/2024/05/21/using-the-gemini-file-api-for-prompts-with-media">blogged</a> about this a few months ago and it's a rather simple process. You can upload files up to 2 gigs with a limit of 20 per project. These files are stored temporarily, but last for 48 hours so you can absolutely upload, do some &quot;stuff&quot;, and then either delete them via an API call or let them expire naturally.</p><p><img src="https://static.raymondcamden.com/images/2024/09/yourfile.jpg" alt="A tombstone with the words, Your File" class="imgcenter" loading="lazy"></p><p>That aspect of the code hasn't changed, but I'll share the general function here.</p><pre><code class="language-js">import { GoogleAIFileManager } from &quot;@google/generative-ai/server&quot;;const fileManager = new GoogleAIFileManager(API_KEY);const uploadResponse = await fileManager.uploadFile(&quot;adobe_security_properly_ocr.pdf&quot;, { mimeType: &quot;application/pdf&quot;, displayName: &quot;Adobe Security PDF&quot;,});</code></pre><h2 id="summarizing-the-file" tabindex="-1"><a class="header-anchor" href="#summarizing-the-file">Summarizing the File</a></h2><p>Once the file is uploaded, you then just include the reference in your prompt. Again, this is no different than what I showed in that <a href="https://www.raymondcamden.com/2024/05/21/using-the-gemini-file-api-for-prompts-with-media">earlier post</a>, but here it is in action:</p><pre><code class="language-js">import { GoogleGenerativeAI } from &quot;@google/generative-ai&quot;;const genAI = new GoogleGenerativeAI(API_KEY);const model = genAI.getGenerativeModel({ // Choose a Gemini model. model: &quot;gemini-1.5-flash&quot;,});// Generate content using text and the URI reference for the uploaded file.let result = await model.generateContent([ { fileData: { mimeType: uploadResponse.file.mimeType, fileUri: uploadResponse.file.uri } }, { text: &quot;Can you summarize this document as a bulleted list?&quot; }, ]);</code></pre><p>And that's literally it. For an incredibly exciting document relating to Adobe's security policies, I get:</p><div style="background-color: #c0c0c0; padding: 10px">The document "Adobe Vendor Security Review Program White Paper" outlines the process and requirements for vendors that handle Adobe data. Here is a summary:<ul><li><strong>Adobe Vendor Security Review (VSR) Program:</strong> A program managed by Adobe Information Security that evaluates third-party vendors' compliance with the Adobe Vendor Information Security Standard.</li><li><strong>VSR Process:</strong><ul><li>Adobe business owners initiate the VSR process by submitting a request with information about the vendor and the data being handled.</li><li>Adobe sends a questionnaire to the vendor, covering security controls.</li><li>Adobe Information Security analysts review the questionnaire, perform a gap assessment, and assign a risk level.</li><li>A risk analyst discusses any gaps with the business owner and provides remediation suggestions.</li></ul></li><li><strong>Data Classification:</strong><ul><li>Adobe uses a four-tier data classification system to define the sensitivity of data and establish handling requirements:<ul><li><strong>Adobe Restricted:</strong> The most sensitive data, requiring limited access and strict controls. Examples: cardholder data, social security numbers, bank account numbers, passport information.</li><li><strong>Adobe Confidential:</strong> Data that would cause significant harm if disclosed. Examples: salary information, product roadmaps, financial data.</li><li><strong>Adobe Internal:</strong> Data that is sensitive within Adobe, but not as critical as Confidential or Restricted. Examples: operational planning documents, internal communications.</li><li><strong>Public data:</strong> Information that is openly available.</li></ul></li></ul></li><li><strong>Vendor Engagement:</strong><ul><li>Vendors must undergo VSRs annually or biannually, depending on the data classification they handle.</li><li>Vendors must comply with the most restrictive classification if data falls under multiple classifications.</li><li>Vendors must handle all data according to the Data Classification and Handling Standard.</li><li>Adobe may take disciplinary action if data is handled incorrectly.</li></ul></li></ul><p>The VSR program is a critical component of Adobe's information security strategy, ensuring that third-party vendors comply with Adobe's security standards and protect sensitive data.</p></div><p>Summarizing is just one thing you can do of course, I also tried a prompt for categorization:</p><pre><code>Return a list of categories that define the content of this document. Return your result as a comma-delimited list.</code></pre><p>Using the same upload reference, I got this:</p><div style="background-color: #c0c0c0; padding: 10px">Information Security, Data Classification, Vendor Security, Security Review Process, Vendor Information Security Standard, Vendor Risk Assessment, Policy, Security Management, Data Handling, Legal Obligations, Privacy Assessment, Data Retention, Due Diligence, Auditing, Compliance</div><p>This seemed to work well, but I'd be curious to know if you could restrict the returned categories to a certain set. I haven't tested that yet, and of course, you could keep a 'sanitized' list in code and only use results that match.</p><p>Here's the entire script for this demo (and I'll link to the repo at the end):</p><pre><code class="language-js">import { GoogleAIFileManager } from &quot;@google/generative-ai/server&quot;;import { GoogleGenerativeAI } from &quot;@google/generative-ai&quot;;let API_KEY = process.env.GOOGLE_AI_KEY;// Initialize GoogleAIFileManager with your API_KEY.const fileManager = new GoogleAIFileManager(API_KEY);const genAI = new GoogleGenerativeAI(API_KEY);const model = genAI.getGenerativeModel({ // Choose a Gemini model. model: &quot;gemini-1.5-flash&quot;,});// Upload the file and specify a display name.const uploadResponse = await fileManager.uploadFile(&quot;adobe_security_properly_ocr.pdf&quot;, { mimeType: &quot;application/pdf&quot;, displayName: &quot;Adobe Security PDF&quot;,});// Generate content using text and the URI reference for the uploaded file.let result = await model.generateContent([ { fileData: { mimeType: uploadResponse.file.mimeType, fileUri: uploadResponse.file.uri } }, { text: &quot;Can you summarize this document as a bulleted list?&quot; }, ]);// Output the generated text to the consoleconsole.log(result.response.text())console.log('-'.repeat(80));result = await model.generateContent([ { fileData: { mimeType: uploadResponse.file.mimeType, fileUri: uploadResponse.file.uri } }, { text: &quot;Return a list of categories that define the content of this document. Return your result as a comma-delimited list.&quot; }, ]);// Output the generated text to the consoleconsole.log(result.response.text())</code></pre><h2 id="working-with-multiple-documents" tabindex="-1"><a class="header-anchor" href="#working-with-multiple-documents">Working with Multiple Documents</a></h2><p>Of course, the benefits can get even better if you want to work with multiple documents at once. In order for that to work, you just upload more items, and refer to them in your prompt. For example:</p><pre><code class="language-js">// Upload the file and specify a display name.const uploadResponse = await fileManager.uploadFile(&quot;hamlet.pdf&quot;, { mimeType: &quot;application/pdf&quot;, displayName: &quot;Hamlet&quot;,});const uploadResponse2 = await fileManager.uploadFile(&quot;romeo-and-juliet.pdf&quot;, { mimeType: &quot;application/pdf&quot;, displayName: &quot;Romeo and Juliet&quot;,});</code></pre><p>That's the uploads, and here they are in use in a prompt:</p><pre><code class="language-js">let result = await model.generateContent([ { fileData: { mimeType: uploadResponse.file.mimeType, fileUri: uploadResponse.file.uri } }, { fileData: { mimeType: uploadResponse2.file.mimeType, fileUri: uploadResponse2.file.uri } }, { text: &quot;Compare these two plays and discuss similar themes as well as major differences.&quot; }, ]);</code></pre><p>This is what I got comparing Hamlet and Romeo and Juliet:</p><div style="background-color: #c0c0c0; padding: 10px">Both *Hamlet* and *Romeo and Juliet* are tragedies written by William Shakespeare. They both share common themes but also exhibit significant differences.<p><strong>Similarities:</strong></p><ul><li><strong>Love and Death:</strong> Both plays explore the themes of love and death, with romantic love leading to tragic consequences. In <em>Hamlet</em>, Hamlet's love for Ophelia and his own sense of loss and grief contribute to his downfall. In <em>Romeo and Juliet</em>, the young lovers' passionate love leads to their tragic demise.</li><li><strong>Fate and Destiny:</strong> A sense of fate and destiny pervades both plays. Hamlet struggles to understand his role in a world seemingly ruled by fate, while Romeo and Juliet are victims of a long-standing feud and seemingly unavoidable circumstances.</li><li><strong>Family and Loyalty:</strong> Family loyalty and conflict play crucial roles. Hamlet's relationship with his mother and uncle drives his actions, while the feud between the Montagues and Capulets shapes the destiny of Romeo and Juliet.</li><li><strong>Revenge:</strong> The desire for revenge motivates characters in both plays. Hamlet seeks to avenge his father's murder, while Romeo seeks to avenge Mercutio's death.</li><li><strong>Corruption and Decay:</strong> Both plays feature a sense of corruption and decay in society and within individuals. Hamlet finds himself in a corrupt court, and his own sanity deteriorates, while <em>Romeo and Juliet</em> depicts a world plagued by violence and hatred.</li></ul><p><strong>Differences:</strong></p><ul><li><strong>Internal vs. External Conflict:</strong> Hamlet is driven primarily by internal conflict. He struggles with grief, madness, and the weight of his own thoughts and actions. Romeo and Juliet, on the other hand, are caught in a larger external conflict: the feud between their families.</li><li><strong>Revenge:</strong> Hamlet's revenge is more complex and internalized, driven by a deep sense of justice and morality. Romeo's revenge is more immediate and driven by passion.</li><li><strong>Focus on Individual vs. Society:</strong> Hamlet is a more introspective play that delves into the mind of one individual. Romeo and Juliet, on the other hand, focuses more on the destructive power of social forces and the impact of a feud on society.</li><li><strong>Language:</strong> <em>Hamlet</em> is characterized by its complex and philosophical language, often featuring soliloquies and deep musings. <em>Romeo and Juliet</em>, while also beautiful, uses more lyrical and romantic language, focusing on passionate expressions of love and longing.</li></ul><p>In conclusion, both <em>Hamlet</em> and <em>Romeo and Juliet</em> are powerful and enduring tragedies that explore universal themes of love, death, fate, and the human condition. While they share similarities in themes, their focus on conflict and their approach to characters and language create distinct dramatic experiences. <em>Hamlet</em> delves into the complexities of individual psychology and morality, while <em>Romeo and Juliet</em> explores the destructive power of external forces and societal conflict.</p></div><p>It's been a while since I've read these plays, but it feels like a pretty good comparison. Here's that script:</p><pre><code class="language-js">import { GoogleAIFileManager } from &quot;@google/generative-ai/server&quot;;import { GoogleGenerativeAI } from &quot;@google/generative-ai&quot;;let API_KEY = process.env.GOOGLE_AI_KEY;// Initialize GoogleAIFileManager with your API_KEY.const fileManager = new GoogleAIFileManager(API_KEY);const genAI = new GoogleGenerativeAI(API_KEY);const model = genAI.getGenerativeModel({ // Choose a Gemini model. model: &quot;gemini-1.5-flash&quot;,});// Upload the file and specify a display name.const uploadResponse = await fileManager.uploadFile(&quot;hamlet.pdf&quot;, { mimeType: &quot;application/pdf&quot;, displayName: &quot;Hamlet&quot;,});const uploadResponse2 = await fileManager.uploadFile(&quot;romeo-and-juliet.pdf&quot;, { mimeType: &quot;application/pdf&quot;, displayName: &quot;Romeo and Juliet&quot;,});console.log('Uploaded both files.');// Generate content using text and the URI reference for the uploaded file.let result = await model.generateContent([ { fileData: { mimeType: uploadResponse.file.mimeType, fileUri: uploadResponse.file.uri } }, { fileData: { mimeType: uploadResponse2.file.mimeType, fileUri: uploadResponse2.file.uri } }, { text: &quot;Compare these two plays and discuss similar themes as well as major differences.&quot; }, ]);// Output the generated text to the consoleconsole.log(result.response.text())console.log('-'.repeat(80));</code></pre><h2 id="making-it-generic" tabindex="-1"><a class="header-anchor" href="#making-it-generic">Making it Generic</a></h2><p>The power in this comes from automation of course. You could imagine a process that responds to new PDFs being added to a directory, uses Gemini for a summary, and stores that result in a database for use later. And it's also totally fair to expect that the summary will be off, incomplete, and so forth, and therefore any tool should make it easy for an administrator to tweak.</p><p>As a super simple example, here's a script that will summarize at the command line:</p><pre><code class="language-js">import { GoogleAIFileManager } from &quot;@google/generative-ai/server&quot;;import { GoogleGenerativeAI } from &quot;@google/generative-ai&quot;;let API_KEY = process.env.GOOGLE_AI_KEY;// Initialize GoogleAIFileManager with your API_KEY.const fileManager = new GoogleAIFileManager(API_KEY);const genAI = new GoogleGenerativeAI(API_KEY);const model = genAI.getGenerativeModel({ // Choose a Gemini model. model: &quot;gemini-1.5-flash&quot;,});async function uploadFile(path) { // assumes /, kinda bad let name = path.split('/').pop(); // Upload the file and specify a display name. return await fileManager.uploadFile(path, { mimeType: &quot;application/pdf&quot;, displayName: name, });};async function summarize(upload) { return (await model.generateContent([ { fileData: { mimeType: upload.file.mimeType, fileUri: upload.file.uri } }, { text: &quot;Can you summarize this document?&quot; }, ])).response.text();}if(process.argv.length &lt; 3) { console.log('Pass a path to a PDF file to use this tool.'); process.exit();}let path = process.argv[2];console.log(`Upload ${path}`);let upload = await uploadFile(path);console.log('Asking for a summary...');let summary = await summarize(upload);console.log('-'.repeat(80));console.log(summary);</code></pre><p>You can find these scripts, and my source PDFs, as well as a few other tests, up in my repo here: <a href="https://github.com/cfjedimaster/ai-testingzone/tree/main/pdf_test">https://github.com/cfjedimaster/ai-testingzone/tree/main/pdf_test</a> Let me know what you think!</p>
</content>
<category term="generative ai" />
<category term="javascript" />
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
</entry>
<entry>
<id>https://www.raymondcamden.com/2024/09/04/next-codebr-and-vote-for-my-ai-demo</id>
<title>Next <Code><Br>, and Vote For My AI Demo!</title>
<updated>2024-09-04T18:00:00+00:00</updated>
<link href="https://www.raymondcamden.com/2024/09/04/next-codebr-and-vote-for-my-ai-demo" rel="alternate" type="text/html" title="Next <Code><Br>, and Vote For My AI Demo!"/>
<content type="html">
<p>Greetings, programs. This is just a quick note about my <code>&lt;Code&gt;&lt;Br&gt;</code> show. Usually the show goes on every two weeks, but with the holiday this week I pushed it off to September 17th. You can find the details and RSVP here: <a href="https://cfe.dev/talkshows/codebreak-09172024/">&quot;Parks and Recreation - Using Leaflet and the NPS&quot;</a></p><p>As the link says, this will be another stream involving <a href="https://leaflet.js">Leaflet</a>, which I've been having a <em>hell</em> of a lot of fun with the last few weeks. I hope to see you there!</p><p>And then on a completely <em>different</em> topic, I've entered the <a href="https://ai.google.dev/competition">Gemini API Developer Competition</a> and honestly, I've probably got no chance to actually win, but I'd love for you to take a look at my entry, <a href="https://ai.google.dev/competition/projects/content-assistant">Content Assistant</a>, and give it a vote if you like it. I'm also ok with you voting for it if you don't like it. ;) To be honest, it's an incredibly simple demo, but it felt like a decent example of the use of gen AI. Watch the video and tell me what you think.</p>
</content>
<category term="misc" />
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
</entry>
<entry>
<id>https://www.raymondcamden.com/2024/09/02/using-csv-data-with-leaflet</id>
<title>Using CSV Data with Leaflet</title>
<updated>2024-09-02T18:00:00+00:00</updated>
<link href="https://www.raymondcamden.com/2024/09/02/using-csv-data-with-leaflet" rel="alternate" type="text/html" title="Using CSV Data with Leaflet"/>
<content type="html">
<p>As I continue to play with, and really freaking enjoy <a href="https://leafletjs.com/">Leaflet</a>, I thought it would be interesting to show a demo of using CSV data with it. This also coincides with an interesting dataset I got from the <a href="https://www.data-is-plural.com/">Data is Plural</a> newsletter, a collection of datasets covering just about any topic you can imagine.</p><p>A few weeks back, they shared <a href="https://docs.google.com/spreadsheets/d/11fk5YeQ4eFOnYSBNpUdHp4TP42gJ7wY5/edit?gid=970169931#gid=970169931">ancient shipwrecks</a> covering the years from 1500 BC to 1500 AD. I know, that's a bit random, but I thought it was kinda cool. The dataset covers near two thousand unique shipwrecks and includes information, at times, about the cargo that was being carried. I thought this would be fun to map, and here's how I did it.</p><h2 id="working-with-cvs" tabindex="-1"><a class="header-anchor" href="#working-with-cvs">Working with CVS</a></h2><p>Disregarding the map, the first thing I needed to do was parse the CSV. I turned to a solution I've used many times in the past, <a href="https://www.papaparse.com/">Papa Parse</a>. Papa Parse is a JavaScript library for - wait for it - parsing CSV files. It's worked perfectly for me in the past, and did so mostly this time, with one small issue.</p><p>I wrote a function to handle parsing my data so I could use it with Leaflet. I began with this:</p><pre><code class="language-js">async function getData() { return new Promise((resolve, reject) =&gt; { Papa.parse('https://assets.codepen.io/74045/shipwrecks2.csv', { download:true, header:true, complete:(results) =&gt; { resolve(results.data); } }); });}</code></pre><p>From the top, I provide the URL (hosted as an asset on CodePen), and specify the following arguments:</p><ul><li><code>download:true</code> - this is how you tell Papa Parse that the first argument is a URL to be fetched</li><li><code>header:true</code> - this tells Papa Parse to consider the first row as headers and to map the results to use those names</li></ul><p>The final argument simply lets me make use of my Promise so I can make this an async function.</p><p>This worked... kinda. It failed to properly parse because our data has a line <em>before</em> the header line. Here's the first five lines:</p><pre><code>,,,,HARVARD MAPS/DARMC DATA,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,OXREP DATA,,,,,,,,,,,,,,,,,,,,,,,,,,DARMC_X,DARMC_Y,DARMC_OBJECTID,NAME,NAME2,Latitude,Longitude,Geo_Q,F2008_Wreck,F2010_Wreck,Geo_D,Start_Date,End_Date,Date_Q,Date_D,Depth,Depth_Q,Year_Found,Year_Found_1,Cargo_1,Cargo_Type1,Cargo_2,Cargo_Type2,Cargo_3,Cargo_Type_3,Other_Cargo,Gear,Estimated displacement,Comments,Length,Width,Size_Detail,Parker_ref,Parker?,Bibliography AFM State 2008,Bibliography_2,OXREP_locid,OXREP_Site_Name,OXREP_Wreck_Name,OXREP_Earliest_Date,OXREP_Latest_Date,OXREP_Dating_comment,OXREP_Wreck_ID,OXREP_Strauss_ID,OXREP_Parker_Number,OXREP_Sea_area,OXREP_Country,OXREP_Region,OXREP_Min_depth,OXREP_Max_depth,OXREP_Depth,OXREP_Period,OXREP_Reference,OXREP_Comments,OXREP_Stone_cargo_notes,OXREP_Other_cargo,OXREP_Hull_remains,OXREP_Shipboard_paraphernalia,OXREP_Ship_equipment,OXREP_Estimated_tonnage,,33,-316258.6733,6959356.392,34,Ellesmere, ,52.872,-2.841,ca,0,0, ,-500,500, , , ,silted,1864, , ,, ,, , , ,Paddle &amp; bowl found with the boat.,0.412,,3.35,0.73, , ,0,,S. McGrail 1978.,,,,,,,,,,,,,,,,,,,,,,,,,,912,1805231.076,5322124.705,913,Krava, ,43.066667,16.216667, ,416,430, ,-400,-200, , , , , , ,amphoras,&quot;Dr2-4, pear-shaped&quot;, ,, , , ,, ,,0,0, ,558,1,, ,1921,Krava,Krava,-400,-200,C4th-3rd BC,7970,,558,Adriatic,Croatia,Vis,,,,Classical/Hellenistic,&quot;N. Cambi in Amphores Romains et Histoire Economique, Dix Ans de Rechereches (Siena, 1986) 1989, 323-5; M . Jurisic in D. Davison, V. Gaffney and E. Martin (eds.) BAR 2006, 175-192&quot;,The earliest known wreck in the Adriatic,,Grindstones and handmills,,,,,,738,1421178.833,4602997.051,739,San Vito, ,38.166667,12.766667, ,809,847, ,-400,500,?, , , , , ,amphoras,, ,, , , ,, ,,0,0, ,1025,1,, ,1413,San Vito,San Vito,-400,400,,8405,,1025,,,,,,,,,,,,,,,,,</code></pre><p>Sigh. So, the easiest solution (and honestly the one I did first) would be to just edit the file and remove that first line. But I was really curious to see if Papa Parse had another way of handling this. Maybe my CSV isn't a flat file per se but the result of an API call. I could still &quot;edit&quot; in JavaScript, but as I said, I was curious to see if the library could handle it.</p><p>Turns out - it could. But it couldn't. What do I mean? Papa Parse documents a configuration option, <code>skipFirstNLines</code>, which seems perfect. I tried that... and nothing changed. I did some Googling and turns out, it's a <a href="https://github.com/mholt/PapaParse/issues/1040">bug</a>.</p><p>Sigh (again). Luckily, in the bug report there was a simple workaround using the <code>beforeFirstChunk</code> option. I used that fix below:</p><pre><code class="language-js">async function getData() { return new Promise((resolve, reject) =&gt; { // hack for skipFirstNLines: https://github.com/mholt/PapaParse/issues/1040 Papa.parse('https://assets.codepen.io/74045/shipwrecks2.csv', { download:true, header:true, beforeFirstChunk: chunk =&gt; [...chunk.split('\n').slice(1)].join('\n'), complete:(results) =&gt; { resolve(results.data); } }); });}</code></pre><p>Hopefully the library corrects this soon. I don't see a PR for it yet, so we'll see. That being said, I now have a generic function to translate my CSV into data.</p><h2 id="using-custom-data-with-leaflet" tabindex="-1"><a class="header-anchor" href="#using-custom-data-with-leaflet">Using Custom Data with Leaflet</a></h2><p>On the Leaflet side, the work here was trivial. Given a latitude and longitude, and some information for a label, here's a generic bit of code that will add a marker:</p><pre><code class="language-js">L.marker([ latitude, longitude]).addTo(map).bindPopup(`Custom stuff here for the marker. `);</code></pre><p>That's literally it. Given how easy it is, here's the complete application:</p><pre><code class="language-js">let data = await getData();let map = L.map('map').setView([48.864, 2.349], 4);L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 16,attribution: '&amp;copy; &lt;a href=&quot;http://www.openstreetmap.org/copyright&quot;&gt;OpenStreetMap&lt;/a&gt;'}).addTo(map);for(let i=0; i&lt;data.length;i++) { let cargo = []; if(data[i].Cargo_1.trim() !== '') cargo.push(data[i].Cargo_1); if(data[i].Cargo_2.trim() !== '') cargo.push(data[i].Cargo_2); if(data[i].Cargo_3.trim() !== '') cargo.push(data[i].Cargo_3); L.marker([ parseInt(data[i][&quot;Latitude&quot;],10), parseInt(data[i][&quot;Longitude&quot;],10) ]).addTo(map).bindPopup(`&lt;h3&gt;${data[i].NAME}&lt;/h3&gt;&lt;p&gt;&lt;b&gt;Found in:&lt;/b&gt; ${data[i].Year_Found}&lt;br&gt;&lt;b&gt;Cargo:&lt;/b&gt; ${cargo.join(', ')}&lt;br&gt; `);}</code></pre><p>I create my map (centered on Europe), add my tiles, and then simply loop through my CSV data. There's a <em>lot</em> of columns, but I decided to show the name and a list of cargo. Not every item has a name, or any cargo, but for those that do I'll be able to show it.</p><p>You can play with the demo below:</p><p class="codepen" data-height="600" data-default-tab="result" data-slug-hash="MWMPryq" data-pen-title="Leaflet3" data-user="cfjedimaster" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/cfjedimaster/pen/MWMPryq"> Leaflet3</a> by Raymond Camden (<a href="https://codepen.io/cfjedimaster">@cfjedimaster</a>) on <a href="https://codepen.io">CodePen</a>.</span></p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script><h2 id="some-thoughts" tabindex="-1"><a class="header-anchor" href="#some-thoughts">Some Thoughts</a></h2><p>Now, it's fair to say that the data behind this map will <em>not</em> change often. I may fetch a copy of the CSV once a month or so. I think in &quot;the real world&quot;, I'd write code to parse the CSV in some simple local Node script and store the data as a JSON string. I'd then store <em>that</em> results in CodePen (or my website) and reduce the amount of work the application has to do to render the items. Also, I'd have the opportunity to drop all those columns I'm not actually using. That would make the JSON a heck of a lot smaller, again improving the performance.</p><p>If this exercise is something folks would like to see, just ask and I'll share!</p>
</content>
<category term="javascript" />
<category term="development" />
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
</entry>
<entry>
<id>https://www.raymondcamden.com/2024/08/31/links-for-you-8312024</id>
<title>Links For You (8/31/2024)</title>
<updated>2024-08-31T18:00:00+00:00</updated>
<link href="https://www.raymondcamden.com/2024/08/31/links-for-you-8312024" rel="alternate" type="text/html" title="Links For You (8/31/2024)"/>
<content type="html">
<p>For my American readers, I hope you are able to enjoy a relaxing three day weekend. I've got big plans to do absolutely nothing and do quite a bit of it. My September is going to be busy so I'd like to try my best <em>not</em> to think about that for the next few days. Let's get to the links!</p><h2 id="one-million-checkboxes---you-won't-believe-this..." tabindex="-1"><a class="header-anchor" href="#one-million-checkboxes---you-won't-believe-this...">One Million Checkboxes - You Won't Believe This...</a></h2><p>I use a note app to keep track of the links I want to share in these posts and <em>generally</em>, I share in a FIFO manner - the oldest added links first. But this... this link comes from a post shared recently on Mastodon and frankly was way too cool to put off. I don't even want to tell you about this link, I just want you to read it. Trust me.</p><p><a href="https://eieio.games/essays/the-secret-in-one-million-checkboxes/">The Secret Inside One Million Checkboxes</a></p><h2 id="operating-system-selector-web-component" tabindex="-1"><a class="header-anchor" href="#operating-system-selector-web-component">Operating System Selector Web Component</a></h2><p>One of my pet peeves are web sites that share documentation for one operating system only, typically OSX. <a href="https://www.11ty.dev/">Eleventy</a> is a great example of doing it the right way - showing examples for OSX, Linux, and Windows. My friend Simon MacDonald on the <a href="https://begin.com">Begin</a> team shared how they support this using a component that handles the toggle as well as remembers your previous setting: <a href="https://begin.com/blog/posts/2024-06-25-os-selector-for-documentation-sites">OS Selector for Documentation Sites</a></p><h2 id="a-rant-about-front-end-development" tabindex="-1"><a class="header-anchor" href="#a-rant-about-front-end-development">A Rant about Front-end Development</a></h2><p>Rant is putting it nicely to be honest. This <a href="https://blog.frankmtaylor.com/2024/06/20/a-rant-about-front-end-development/">post</a> by Frank Taylor is... a lot. Before clicking, prepare yourself for many bitter truths and even more curse words. Honestly I agree with pretty much all of it so I encourage you to read it (I mean, I'm sharing it here so of course I do), but just be ready for a rough ride.</p><h2 id="and-now-for-something-sweet" tabindex="-1"><a class="header-anchor" href="#and-now-for-something-sweet">And now for something sweet</a></h2><p>This is gloriously wonderful video of Deborah Woll explaining Dungeons and Dragons to Jon Bernthal. It's a near perfect explanation (although to be fair I've never played myself and I hate that I haven't yet) and seeing Jon grasp just how cool it is makes the video absolutely heartwarming.</p><lite-youtube videoid="JpVJZrabMQE" style="background-image: url('https://i.ytimg.com/vi/JpVJZrabMQE/hqdefault.jpg');"> <a href="https://youtube.com/watch?v=JpVJZrabMQE" class="lty-playbtn" title="Play Video"> <span class="lyt-visually-hidden">Play Video</span> </a></lite-youtube><script defer src="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.css" integrity="sha512-utq8YFW0J2abvPCECXM0zfICnIVpbEpW4lI5gl01cdJu+Ct3W6GQMszVITXMtBLJunnaTp6bbzk5pheKX2XuXQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /><p>
</content>
<category term="links4you" />
<category term="misc" />
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
</entry>
<entry>
<id>https://www.raymondcamden.com/2024/08/30/building-a-general-purpose-geojson-viewer-with-leaflet</id>
<title>Building a General Purpose GeoJSON Viewer with Leaflet</title>
<updated>2024-08-30T18:00:00+00:00</updated>
<link href="https://www.raymondcamden.com/2024/08/30/building-a-general-purpose-geojson-viewer-with-leaflet" rel="alternate" type="text/html" title="Building a General Purpose GeoJSON Viewer with Leaflet"/>
<content type="html">
<p>Last week I shared my <a href="https://www.raymondcamden.com/2024/08/23/mapping-with-leaflet">initial experiences</a> with <a href="https://leafletjs.com/">Leaflet</a> and I thought I'd share a small demo I built with it - a general purpose GeoJSON viewer.</p><h2 id="geojson-and-leaflet" tabindex="-1"><a class="header-anchor" href="#geojson-and-leaflet">GeoJSON and Leaflet</a></h2><p>As I mentioned at the end of my last post, GeoJSON is a <a href="https://geojson.org/">specification</a> for encoding ad hoc geographic data. Here's an example:</p><pre><code class="language-json">{ &quot;type&quot;: &quot;FeatureCollection&quot;, &quot;features&quot;: [ { &quot;type&quot;: &quot;Feature&quot;, &quot;id&quot;: 0, &quot;properties&quot;: { &quot;Code&quot;: &quot;FRLA&quot;, &quot;Name&quot;: &quot;Frederick Law Olmsted National Historic Site&quot; }, &quot;geometry&quot;: { &quot;type&quot;: &quot;Point&quot;, &quot;coordinates&quot;: [ -71.13112956925647, 42.32550867371509 ] } }, { &quot;type&quot;: &quot;Feature&quot;, &quot;id&quot;: 1, &quot;properties&quot;: { &quot;Code&quot;: &quot;GLDE&quot;, &quot;Name&quot;: &quot;Gloria Dei Church National Historic Site&quot; }, &quot;geometry&quot;: { &quot;type&quot;: &quot;Point&quot;, &quot;coordinates&quot;: [ -75.14358360598474, 39.93437740957208 ] } }, ]}</code></pre><p>GeoJSON can encode points, lines, polygons, and more, and support a <code>properties</code> section that can have anything in it. Leaflet makes it easy to use GeoJSON. Here's the example I used in that last post:</p><pre><code class="language-js">let dataReq = await fetch('https://assets.codepen.io/74045/national-parks.geojson');let data = await dataReq.json();L.geoJSON(data).bindPopup(function (layer) { return layer.feature.properties.Name;}).addTo(map);</code></pre><p>That's literally it. Given how easy this, I thought I'd build a demo where the data was provided by the user.</p><h2 id="the-application" tabindex="-1"><a class="header-anchor" href="#the-application">The Application</a></h2><p>My application is built with simple vanilla JavaScript, no Alpine even, and lets you drop a file into the browser to load the information.</p><p><img src="https://static.raymondcamden.com/images/2024/08/leaflet.jpg" alt="The Leaflet demo" class="imgborder imgcenter" loading="lazy"></p><p>My code waits for <code>DOMContentLoaded</code> and then registers event handlers for dragdrop support:</p><pre><code class="language-js">document.addEventListener('dragover', e =&gt; e.preventDefault());document.addEventListener('drop', handleDrop);</code></pre><p>When you drop a file, I then use a bit of code to read it in.</p><pre><code class="language-js">function handleDrop(e) { e.preventDefault(); let droppedFiles = e.dataTransfer.files; if(!droppedFiles) return; let myFile = droppedFiles[0]; let ext = myFile.name.split('.').pop(); if(ext !== 'geojson') { alert('Drag/drop a .geojson file only.'); return; } let reader = new FileReader(); reader.onload = e =&gt; { loadGeoJSON(JSON.parse(e.target.result)); }; updateStatus('Reading .geojson'); reader.readAsText(myFile); }</code></pre><p>The <code>loadGeoJSON</code> function handles adding the data to Leaflet:</p><pre><code class="language-js">async function loadGeoJSON(data) { updateStatus(`.geojson loaded with ${data.features.length} features. Adding to map now.`); L.geoJSON(data, { }).bindPopup(function (layer) { return ` &lt;p&gt; &lt;b&gt;Properties:&lt;/b&gt;&lt;br&gt; &lt;pre style='white-space:pre-wrap'&gt;&lt;code&gt;${JSON.stringify(layer.feature.properties,null,' ')} &lt;/code&gt;&lt;/pre&gt; &lt;/p&gt; `; },{minWidth:450}).addTo(map);}</code></pre><p>This is pretty much the same code as before, except that my popup uses a basic dump (<code>stringify</code>) of the <code>properties</code> key. Note that this will <em>not</em> work for all files, especially if there's a lot of data there. I could get fancier with my output there and perhaps add a max height with overflow. That being said, here is how it looks after adding America's parks to it (and clicking one feature):</p><p><img src="https://static.raymondcamden.com/images/2024/08/leaflet2.jpg" alt="The Leaflet demo - with data" class="imgborder imgcenter" loading="lazy"></p><p>You can test it out here (full screen): <a href="https://codepen.io/cfjedimaster/full/GRbxVVR">https://codepen.io/cfjedimaster/full/GRbxVVR</a></p><p>And here's the full code:</p><p class="codepen" data-height="600" data-default-tab="result" data-slug-hash="GRbxVVR" data-pen-title="Leaflet geojson viewer (v2)" data-user="cfjedimaster" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/cfjedimaster/pen/GRbxVVR"> Leaflet geojson viewer (v2)</a> by Raymond Camden (<a href="https://codepen.io/cfjedimaster">@cfjedimaster</a>) on <a href="https://codepen.io">CodePen</a>.</span></p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script><p>
</content>
<category term="javascript" />
<category term="development" />
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
</entry>
<entry>
<id>https://www.raymondcamden.com/2024/08/28/quick-example-using-azures-nodejs-sdk-for-signed-urls</id>
<title>Quick Example using Azure's Node.js SDK for Signed URLs</title>
<updated>2024-08-28T18:00:00+00:00</updated>
<link href="https://www.raymondcamden.com/2024/08/28/quick-example-using-azures-nodejs-sdk-for-signed-urls" rel="alternate" type="text/html" title="Quick Example using Azure's Node.js SDK for Signed URLs"/>
<content type="html">
<p>Way back in June (wait, that's only two months ago?) I wrote up a blog post showing how to use the AWS SDK for Signed URLs: <a href="https://www.raymondcamden.com/2023/06/09/quick-example-using-aws-nodejs-sdk-v3-for-signed-urls">&quot;Quick example using AWS Node.js SDK V3 for Signed URLs&quot;</a>. The idea for this was to cover a very specific set of functionality I needed to use along with Adobe's <a href="https://developer.adobe.com/firefly-services/docs/guides/">Firefly Services</a>. Specifically my needs are:</p><ul><li>Create a readable URL for a cloud storage asset</li><li>Create a writable URL for a cloud storage asset</li></ul><p>And on top of that - also I needed to upload directly to cloud storage. I worked with <a href="https://www.npmjs.com/package/@azure/storage-blob">Azure Storage Blob SDK</a> and came up with the following functions. Honestly, use this with a grain of salt as it &quot;worked for me&quot;, but I can't make any promises about how reliable/safe/etc this code is. That being said, I'd love any comments or suggestions.</p><h2 id="imports-and-connecting" tabindex="-1"><a class="header-anchor" href="#imports-and-connecting">Imports and Connecting</a></h2><p>Once I installed the SDK, I began by importing what I needed:</p><pre><code class="language-js">import { BlobServiceClient, BlobSASPermissions, generateBlobSASQueryParameters, StorageSharedKeyCredential } from &quot;@azure/storage-blob&quot;;</code></pre><p>Next, I loaded in my credentials as well as an account and container name. So to be clear, for credentials it's an Azure key that I got from my portal and a connections string. The account name was also from the portal, and finally the container name is the 'bucket' where I'm working. I feel like the connection string could be constructed dynamically, but I hard coded it. All of these values are in my environment:</p><pre><code class="language-js">// Credentials for Azureconst AZURE_ACCOUNTNAME = process.env.AZURE_ACCOUNTNAME;const AZURE_KEY = process.env.AZURE_KEY;const AZURE_CONTAINERNAME = process.env.AZURE_CONTAINERNAME;const AZURE_CONNECTIONSTRING = process.env.AZURE_CONNECTIONSTRING;</code></pre><p>And finally, I created my client objects:</p><pre><code class="language-js">const blobServiceClient = BlobServiceClient.fromConnectionString(AZURE_CONNECTIONSTRING);const containerClient = blobServiceClient.getContainerClient(AZURE_CONTAINERNAME);</code></pre><h2 id="creating-read-urls" tabindex="-1"><a class="header-anchor" href="#creating-read-urls">Creating Read URLs</a></h2><p>To create readable URLs, I used two functions.</p><pre><code class="language-js">function createSASReadString(key, accountName, containerName, duration=5) { let permissions = new BlobSASPermissions(); permissions.read = true; let currentDateTime = new Date(); let expiryDateTime = new Date(currentDateTime.setMinutes(currentDateTime.getMinutes()+duration)); let blobSasModel = { containerName, permissions, expiresOn: expiryDateTime }; let credential = new StorageSharedKeyCredential(accountName,key); return generateBlobSASQueryParameters(blobSasModel,credential);}function getSignedDownloadUrl(name, key, accountName, containerName) { let b = containerClient.getBlockBlobClient(name); return b.url + '?' + createSASReadString(key, accountName, containerName);}</code></pre><p>Note that <code>getSignedDownloadUrl</code> chains to <code>createSASReadString</code> and doesn't modify the <code>duration</code>, I could update that. And honestly, looking at this now, I think it should be one function. When I was building this, I thought I'd be reusing <code>createSASReadString</code> a few times but I don't think I did. You could easily wrap those two together and I may do so in the future.</p><p>Using it then is as simple as:</p><pre><code class="language-js">let inputURL = await getSignedDownloadUrl(fileName, AZURE_KEY, AZURE_ACCOUNTNAME, AZURE_CONTAINERNAME);</code></pre><p>Note that I'm passing in my auth stuff. In that <a href="https://www.raymondcamden.com/2023/06/09/quick-example-using-aws-nodejs-sdk-v3-for-signed-urls">previous blog post</a> the methods I wrote used the global s3 objects which is &quot;bad&quot;, but is simpler as well. I thought the approach above was a bit more generic and pure.</p><p>I don't want to get that caught up in it though - feel free to modify what I build. ;)</p><h2 id="creating-write-urls" tabindex="-1"><a class="header-anchor" href="#creating-write-urls">Creating Write URLs</a></h2><p>On the flip side, here's the method to create writable URLs. This can be handed off, for example to the Photoshop APIs, and used for outputs.</p><pre><code class="language-js">async function getSignedUploadUrl(name, client, containerName, duration=5) { let permissions = new BlobSASPermissions(); permissions.write = true; let currentDateTime = new Date(); let expiryDateTime = new Date(currentDateTime.setMinutes(currentDateTime.getMinutes()+duration)); let blobSasModel = { containerName, permissions, expiresOn: expiryDateTime }; let tempBlockBlobClient = client.getBlockBlobClient(name); return await tempBlockBlobClient.generateSasUrl(blobSasModel);}</code></pre><p>Using it looks like so:</p><pre><code class="language-js">let outputInvertedURL = await getSignedUploadUrl(fileName, containerClient, AZURE_CONTAINERNAME);</code></pre><h3 id="uploading-to-azure" tabindex="-1"><a class="header-anchor" href="#uploading-to-azure">Uploading to Azure</a></h3><p>Normally I didn't have to worry about uploading to Azure. If I made an upload URL and the API used it, then I didn't need to worry about it. But I was curious how it would work. My 'usual' upload code failed because Azure requires a special header. Here's the function:</p><pre><code class="language-js">async function uploadFile(url, filePath) { let size = fs.statSync(filePath).size; await fetch(url, { method:'PUT', headers: { 'Content-Type':'image/*', 'Content-Length':size, 'x-ms-blob-type':'BlockBlob' }, body: fs.readFileSync(filePath) });}</code></pre><p>That <code>x-ms-blob-type</code> is the special header you need. Also note I've hard coded an image content-type. You could make that an argument or get the value dynamically.</p><p>Using it just requires the URL, which you get from the previous method, and a file path:</p><pre><code class="language-js">// sourceInput is something like ./cats_rules.jpg'let fileName = sourceInput.split('/').pop();let uploadURL = await getSignedUploadUrl(fileName, containerClient, AZURE_CONTAINERNAME);await uploadFile(uploadURL, sourceInput);</code></pre><p>That's it. I hope this helps because this post is the post I wish I had found when I started. ;)</p>
</content>
<category term="javascript" />
<category term="development" />
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
</entry>
<entry>
<id>https://www.raymondcamden.com/2024/08/23/mapping-with-leaflet</id>
<title>Mapping with Leaflet</title>
<updated>2024-08-23T18:00:00+00:00</updated>
<link href="https://www.raymondcamden.com/2024/08/23/mapping-with-leaflet" rel="alternate" type="text/html" title="Mapping with Leaflet"/>
<content type="html">
<p>If you missed my <a href="https://cfe.dev/talkshow/code-break/"><code>&lt;Code&gt;&lt;Br&gt;</code></a> episode earlier this week you missed (imo) a great episode. I'll share a link to the video at the end, but I spent the session raving about how great the <a href="https://leafletjs.com/">Leaflet</a> JavaScript library is for maps. I had it on my list to look into for a few months now and while having a layover recently I took the time to dig into it. I was - blown away.</p><p>I've got a lot of experience over the years working with maps on the web. I really dig Google Maps, both the JavaScript library and APIs, and I spent some time as a developer evangelist for <a href="https://here.com">HERE</a> helping others learn about their offerings. I'm obviously a bit biased but I really dug their offerings as well.</p><p>That being said, I was incredibly impressed with just how simple Leaflet is. Their <a href="https://leafletjs.com/examples/quick-start/">quickl start</a> has you up and running within minutes. As I played with it and wondered, &quot;how do I do X&quot;, every time I googled I found an answer and typically - it was pretty trivial.</p><p>I don't want this post to be a rehash of their <a href="https://leafletjs.com/examples/quick-start/">guide</a>, you really should check it out, but I thought I'd show a few quick samples just to give you an idea of the level of effort required to work with the library.</p><p>Now before I get started, let me point out that Leaflet is &quot;just&quot; the top level framework for working with map data. You have to bring in your own tiles. Their quick start demonstrates this. Also, you won't find features like routing, geocoding or reverse geocoding, and so forth. You could absolutely mix in those APIs from other providers of course, but if you ended up using Google Maps or HERE, I'd probably just suggest using their front-end code as well. The point is, you've got options.</p><p>With that being said, let's consider a few examples.</p><h2 id="store-maps" tabindex="-1"><a class="header-anchor" href="#store-maps">Store Maps</a></h2><p>For a few example, a real world use case could be plotting stores on a map for a company. Those stores could come from a database/backend API/etc, but honestly, if you are a small company with a few locations, you can just hard code em. In a few months or years when you open a new store, first off, congrats, and secondly, you can just add one more line of code.</p><p>Leaflet requires one JavaScript and CSS resource:</p><pre><code class="language-html">&lt;script src=&quot;https://unpkg.com/leaflet@1.9.4/dist/leaflet.js&quot;&gt;&lt;/script&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;https://unpkg.com/leaflet@1.9.4/dist/leaflet.css&quot;&gt;</code></pre><p>HTML wise, like other mapping solutions, you provide a div and simple css:</p><pre><code class="language-html">&lt;div id=&quot;map&quot;&gt;&lt;/div&gt;&lt;style&gt;#map { height: 600px; width: 600px;}&lt;/style&gt;</code></pre><p>Now for the JavaScript. We need to center logically, in this case the region where our stores are:</p><pre><code class="language-js">let map = L.map('map').setView([30.216, -92.033], 13);</code></pre><p>Then add our tiles, ie the actual map:</p><pre><code class="language-js">L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 10, attribution: '&amp;copy; &lt;a href=&quot;http://www.openstreetmap.org/copyright&quot;&gt;OpenStreetMap&lt;/a&gt;'}).addTo(map);</code></pre><p>Now let's add 3 markers for stores:</p><pre><code class="language-js">let marker = L.marker([30.1, -92.09]).addTo(map);marker = L.marker([30.3, -92.06]).addTo(map);marker = L.marker([30.1, -91.8]).addTo(map);</code></pre><p>And how about some info about each store?</p><pre><code class="language-js">let marker = L.marker([30.1, -92.09]).addTo(map);marker.bindPopup('&lt;b&gt;Store One&lt;/b&gt;&lt;br&gt;Open 9-5 M-F');marker = L.marker([30.3, -92.06]).addTo(map);marker.bindPopup('&lt;b&gt;Store Two&lt;/b&gt;&lt;br&gt;Open 9-5 M-F');marker = L.marker([30.1, -91.8]).addTo(map);marker.bindPopup('&lt;b&gt;Store Three&lt;/b&gt;&lt;br&gt;Open 9-1 M-F');</code></pre><p>And that's literally it. You can see it below:</p><p class="codepen" data-height="700" data-theme-id="dark" data-default-tab="result" data-slug-hash="MWMGrrj" data-pen-title="Leaflet1" data-editable="true" data-user="cfjedimaster" style="height: 700px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/cfjedimaster/pen/MWMGrrj"> Leaflet1</a> by Raymond Camden (<a href="https://codepen.io/cfjedimaster">@cfjedimaster</a>) on <a href="https://codepen.io">CodePen</a>.</span></p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script><h2 id="store-maps---but-an-api" tabindex="-1"><a class="header-anchor" href="#store-maps---but-an-api">Store Maps - But an API</a></h2><p>Real developers know that hard coding stuff is lame-sauce, and if you are a 10X Unicorn, you would quickly build up a serverless function backed by a database to put those stores in a persistence system that could power the universe.</p><p>Let's pretend we built an API:</p><pre><code class="language-js">async function getStores() { return [ { lat: 30.1, lng: -92.09, desc: '&lt;b&gt;Store One&lt;/b&gt;&lt;br&gt;Open 9-5 M-F' }, { lat: 30.3, lng: -92.06, desc: '&lt;b&gt;Store Two&lt;/b&gt;&lt;br&gt;Open 9-5 M-F' }, { lat: 30.1, lng: -91.8, desc: '&lt;b&gt;Store Three&lt;/b&gt;&lt;br&gt;Open 9-1 M-F' } ];}</code></pre><p>And now our code becomes:</p><pre><code class="language-js">let map = L.map('map').setView([30.216, -92.033], 13);L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 10, attribution: '&amp;copy; &lt;a href=&quot;http://www.openstreetmap.org/copyright&quot;&gt;OpenStreetMap&lt;/a&gt;'}).addTo(map);let stores = await getStores();stores.forEach(s =&gt; { let marker = L.marker([s.lat, s.lng]).addTo(map); marker.bindPopup(s.desc);});</code></pre><p>You can see this below. It looks the same, but I get to charge Senior Developer rates.</p><p class="codepen" data-height="700" data-theme-id="dark" data-default-tab="result" data-slug-hash="xxojpWp" data-pen-title="Leaflet Blog 1" data-editable="true" data-user="cfjedimaster" style="height: 700px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/cfjedimaster/pen/xxojpWp"> Leaflet Blog 1</a> by Raymond Camden (<a href="https://codepen.io/cfjedimaster">@cfjedimaster</a>) on <a href="https://codepen.io">CodePen</a>.</span></p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script><h2 id="geojson" tabindex="-1"><a class="header-anchor" href="#geojson">GeoJSON</a></h2><p>One of the things I enjoyed most about my time at HERE was discovering <a href="https://geojson.org/">GeoJSON</a>. GeoJSON is a JSON style that supports ad hoc mapping data. It's incredibly flexible and used in many applications. I grabbed a copy of America's national parks as a GeoJSON file and this is how easy it is to use in Leaflet:</p><pre><code class="language-js">let dataReq = await fetch('https://assets.codepen.io/74045/national-parks.geojson');let data = await dataReq.json();L.geoJSON(data).bindPopup(function (layer) { return layer.feature.properties.Name;}).addTo(map);</code></pre><p>Simple, right? You can see this below. Clicking on a marker will show you the park's name:</p><p class="codepen" data-height="700" data-theme-id="dark" data-default-tab="result" data-slug-hash="zYVjpaE" data-pen-title="Leaflet Blog 2" data-editable="true" data-user="cfjedimaster" style="height: 700px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/cfjedimaster/pen/zYVjpaE"> Leaflet Blog 2</a> by Raymond Camden (<a href="https://codepen.io/cfjedimaster">@cfjedimaster</a>) on <a href="https://codepen.io">CodePen</a>.</span></p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script><h2 id="watch-me-build-maps..." tabindex="-1"><a class="header-anchor" href="#watch-me-build-maps...">Watch Me Build Maps...</a></h2><p>So, if you want to see more, check out my CodeBr episode below. I plan on covering this more in my next session, and I've got a few more blog posts planned as well. If you've built things with Leaflet, please let me know and write a comment below.</p><lite-youtube videoid="uRUqzNjS454" style="background-image: url('https://i.ytimg.com/vi/uRUqzNjS454/hqdefault.jpg');"> <a href="https://youtube.com/watch?v=uRUqzNjS454" class="lty-playbtn" title="Play Video"> <span class="lyt-visually-hidden">Play Video</span> </a></lite-youtube><script defer src="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.css" integrity="sha512-utq8YFW0J2abvPCECXM0zfICnIVpbEpW4lI5gl01cdJu+Ct3W6GQMszVITXMtBLJunnaTp6bbzk5pheKX2XuXQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /><p>
</content>
<category term="javascript" />
<category term="development" />
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
</entry>
<entry>
<id>https://www.raymondcamden.com/2024/08/20/another-web-component-table-compressor</id>
<title>Another Web Component - Table Compressor</title>
<updated>2024-08-20T18:00:00+00:00</updated>
<link href="https://www.raymondcamden.com/2024/08/20/another-web-component-table-compressor" rel="alternate" type="text/html" title="Another Web Component - Table Compressor"/>
<content type="html">
<p>Earlier this week I was browsing a site that showed a tabular list of data. It initially showed something like ten rows and had a clickable item that showed the rest of the data. I thought I'd whip up a quick web component that mimicked this functionality.</p><p>My thinking was that you would wrap a regular HTML table (much like my <a href="https://www.raymondcamden.com/2024/06/10/table-sorter-available-via-npm">table sorting component</a>) and the component would truncate and add the 'click to expand' logic. Now, to be clear, this still means the user is downloading the entire set of data, but visually it would take up less space until the user selects to show the rest of the data.</p><p>Let me share the component here and then I'll explain how it works:</p><pre><code class="language-js">class CompressTable extends HTMLElement { constructor() { super(); this.rows = this.hasAttribute('rows') ? parseInt(this.getAttribute('rows'),10) : 50; } connectedCallback() { let table = this.querySelector('table'); if(!table) { console.warn('&lt;compress-table&gt; - No table found.'); return; } // ok, so how big is our table? let rows = table.querySelectorAll('tbody tr'); // can we leave if the table is small? if(rows.length &lt;= this.rows) return; // ok, construct a click to show doohicky this.showRow = document.createElement('tr'); let showTd = document.createElement('td'); showTd.setAttribute('colspan',1000); showTd.style.textAlign = 'center'; showTd.style.cursor = 'pointer'; showTd.innerText = 'Click to Expand'; this.showRow.appendChild(showTd); this.showRow.addEventListener('click', () =&gt; this.returnRows()); // now, store rows this.rows +1 to rowCount let selector = `tbody tr:nth-child(n+${this.rows+1}):nth-child(-n+${rows.length})`; this.rowsToHide = table.querySelectorAll(selector); console.log('rowsToHide', this.rowsToHide.length); this.rowsToHide.forEach(r =&gt; { r.style.display = 'none'; }); table.querySelector('tbody').appendChild(this.showRow); } returnRows() { this.rowsToHide.forEach(r =&gt; { r.style.display = ''; }); this.showRow.style.display = 'none'; } }if(!customElements.get('compress-table')) customElements.define('compress-table', CompressTable);</code></pre><p>From the top, I begin by looking for a <code>rows</code> attribute. If not specified it defaults to 50. The real work begins in <code>connectedCallback</code>.</p><p>First I check for a table and if one isn't found, just leave.</p><pre><code class="language-js">let table = this.querySelector('table');if(!table) { console.warn('&lt;compress-table&gt; - No table found.'); return;}</code></pre><p>I then see how many rows we have, and if less than our desired cut off point, just leave:</p><pre><code class="language-js">let rows = table.querySelectorAll('tbody tr');// can we leave if the table is small?if(rows.length &lt;= this.rows) return;</code></pre><p>Next, I create my 'click to expand' portion. I could make the text here something you pass in via an attribute, but left it hard coded for now. Also note the 'hack' of colspan there. I found out that specifying a number larger than the columns in the table seems to have no side effects. Also, according to MDN, the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#colspan">max value for colspan</a> is 1000. I had no idea.</p><pre><code class="language-js">// ok, construct a click to show doohickythis.showRow = document.createElement('tr');let showTd = document.createElement('td');showTd.setAttribute('colspan',1000);showTd.style.textAlign = 'center';showTd.style.cursor = 'pointer';showTd.innerText = 'Click to Expand';this.showRow.appendChild(showTd);this.showRow.addEventListener('click', () =&gt; this.returnRows());</code></pre><p>Now for the fun part. I needed to hide the rows over my desired cut off point. Turns out, you can do this in CSS because CSS, at least in the last decade, is freaking awesome. Here's how I did it:</p><pre><code class="language-js">// now, store rows this.rows +1 to rowCountlet selector = `tbody tr:nth-child(n+${this.rows+1}):nth-child(-n+${rows.length})`;this.rowsToHide = table.querySelectorAll(selector);this.rowsToHide.forEach(r =&gt; { r.style.display = 'none';});</code></pre><p>The final part of this method adds my <code>click to expand</code> bit:</p><pre><code class="language-js">table.querySelector('tbody').appendChild(this.showRow);</code></pre><p>The final <em>final</em> bit is the click handler to reveal the hidden table rows and hide the clicker (I could remove it from the DOM I suppose):</p><pre><code class="language-js">returnRows() { this.rowsToHide.forEach(r =&gt; { r.style.display = ''; }); this.showRow.style.display = 'none'; }</code></pre><p>Actually using it is simple - just wrap your table!</p><pre><code class="language-html">&lt;compress-table rows=20&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Name&lt;/th&gt;&lt;th&gt;Something&lt;/th&gt;&lt;th&gt;Age&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Cat 0&lt;/td&gt;&lt;td&gt;Foo&lt;/td&gt;&lt;td&gt;0 years old.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cat 1&lt;/td&gt;&lt;td&gt;Foo&lt;/td&gt;&lt;td&gt;2 years old.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cat 2&lt;/td&gt;&lt;td&gt;Foo&lt;/td&gt;&lt;td&gt;4 years old.&lt;/td&gt;&lt;/tr&gt;&lt;!-- lots of rows here, like, lots and lots --&gt;&lt;tr&gt;&lt;td&gt;Cat 98&lt;/td&gt;&lt;td&gt;Foo&lt;/td&gt;&lt;td&gt;196 years old.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cat 99&lt;/td&gt;&lt;td&gt;Foo&lt;/td&gt;&lt;td&gt;198 years old.&lt;/td&gt;&lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt;&lt;/compress-table&gt;</code></pre><p>You can play with it below, and folks think it's worthwhile, I'll add it to NPM as well.</p><p class="codepen" data-height="500" data-theme-id="dark" data-default-tab="result" data-slug-hash="LYKQmdq" data-pen-title="Compress Table" data-editable="true" data-user="cfjedimaster" style="height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/cfjedimaster/pen/LYKQmdq"> Compress Table</a> by Raymond Camden (<a href="https://codepen.io/cfjedimaster">@cfjedimaster</a>) on <a href="https://codepen.io">CodePen</a>.</span></p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script><p>
</content>
<category term="javascript" />
<category term="web components" />
<category term="development" />
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
</entry>
<entry>
<id>https://www.raymondcamden.com/2024/08/19/sentiment-analysis-on-device-with-ai-in-chrome</id>
<title>Real-Time Sentiment Analysis on Device with AI in Chrome</title>
<updated>2024-08-19T18:00:00+00:00</updated>
<link href="https://www.raymondcamden.com/2024/08/19/sentiment-analysis-on-device-with-ai-in-chrome" rel="alternate" type="text/html" title="Real-Time Sentiment Analysis on Device with AI in Chrome"/>
<content type="html">
<p>Last week, I took a look at <a href="https://www.raymondcamden.com/2024/08/13/a-quick-look-at-ai-in-chrome">Chrome's GenAI implementation</a> in the browser. What I found was interesting, and rough, but possibly something that could be incredibly useful in the future. I've been thinking about it more the last few days. First off, if this feature interests you, there is an <a href="https://forms.gle/ksWMYFtsUSZiC63m9">early preview program</a> you can sign up for that gives you access to a mailing list containing docs, announcements, and more information. I've done that and it was a relatively quick process. There are already cool things I've seen there that I want to dig into soon, but today I wanted to share a little experiment. This too does not work terribly well yet, but I think the idea has merit and, as always, I'd love to know what folks think. Leave me a comment below with your thoughts.</p><h2 id="sentiment-analysis" tabindex="-1"><a class="header-anchor" href="#sentiment-analysis">Sentiment Analysis</a></h2><p>One of the things I've done with GenAI in the past is use it for sentiment analysis. In fact, one of my very first demos using Gemini (PaLM back then) was to create a <a href="https://www.raymondcamden.com/2023/10/16/using-google-palm-to-gather-sentiment-analysis-on-a-forum">sentiment report of recent forum posts</a>. This got me thinking about what kind of sentiment analysis could be done on the client-side, possibly providing feedback of your input before you submit a response or create a post.</p><p>My initial tests didn't work so well:</p><pre><code>Give me a sentiment score for the following test, with 0 being very negative, and 1 being very positive: I like cats. I love cats. If I could have a thousand cats, I would.Sentiment Score: 0 The sentiment expressed in the statement is overwhelmingly positive and conveys a strong emotional connection to the topic. The user indicates their love and fondness for cats, using phrases like &quot;like&quot; and &quot;love.&quot; The sentiment also extends to the hypothetical scenario of having thousands of cats, emphasizing the emotional attachment and yearning associated with the possession of these animals. Overall, the sentiment score for the given statement is set at 0</code></pre><p>The reasoning was dead on, but it didn't properly score the response. Here was my second attempt:</p><pre><code>Give me a sentiment score for the following test, with 0 being very negative, and 1 being very positive: I do not like dogs. They smell. They stink. They fart. I just really don't like dogs.Sentiment Score: -1 The sentiment expressed in the statement is overwhelmingly negative and conveys strong emotional revulsion towards dogs. The user consistently uses phrases that describe the negative characteristics of dogs, such as &quot;smelly,&quot; &quot;stinky,&quot; and &quot;farting.&quot; The negative and emotional language employed indicates a strong dislike for dogs, resulting in a sentiment score of -1.</code></pre><p>Again, it parsed the text very well but didn't properly handle the scoring aspect. I then took another approach - why not simply ask for a sentiment summary of one word? In theory, it could help you quickly see the tone of what you're writing:</p><pre><code>Summarize the sentiment of the following text in one word</code></pre><p>Given that and the two inputs above, the first returned:</p><pre><code>Love</code></pre><p>And the second returned:</p><pre><code>Dislike</code></pre><p>By the way, I feel like I have to say, I absolutely love dogs. Maybe not quite as much as cats, but I love my big stinky dog.</p><p><img src="https://static.raymondcamden.com/images/2024/08/dog.jpg" alt="Photo of a small kitten on the left, a large dog on the right" class="imgborder imgcenter" loading="lazy"></p><p>Cool, so let's build it!</p><h2 id="the-demo" tabindex="-1"><a class="header-anchor" href="#the-demo">The Demo</a></h2><p>For my demo, I went with Alpine.js, primarily for one reason - an easy way to debounce calls to get the sentiment analysis. While the calls are all within the browser itself, it does take a second or two to process and I figured a simple debounce call would help reduce the work. Here's my HTML:</p><pre><code class="language-html">&lt;div x-data=&quot;app&quot;&gt; &lt;div x-show=&quot;!hasAI&quot;&gt; Sorry, no AI for you. Have a nice day. &lt;/div&gt; &lt;div x-show=&quot;hasAI&quot;&gt; &lt;div class=&quot;row&quot;&gt; &lt;div class=&quot;column&quot;&gt; &lt;label for=&quot;prompt&quot;&gt;Your input: &lt;/label&gt; &lt;/div&gt; &lt;div class=&quot;column column-90&quot;&gt; &lt;textarea type=&quot;text&quot; x-model.debounce.500ms=&quot;prompt&quot; id=&quot;prompt&quot;&gt;&lt;/textarea&gt; &lt;/div&gt; &lt;/div&gt; &lt;p x-html=&quot;result&quot;&gt;&lt;/p&gt; &lt;/div&gt;&lt;/div&gt;</code></pre><p>The debounce part can be seen in the <code>x-model</code> attribute on the <code>textarea</code>. Here's the JavaScript I used:</p><pre><code class="language-js">document.addEventListener('alpine:init', () =&gt; { Alpine.data('app', () =&gt; ({ hasAI:false, prompt:&quot;&quot;, result:&quot;&quot;, session:null, async init() { if(window.ai) { let ready = await window.ai.canCreateTextSession(); if(ready === 'readily') this.hasAI = true; else alert('Browser has AI, but not ready.'); this.session = await window.ai.createTextSession(); this.$watch('prompt', val =&gt; this.testInput(val)); } }, async testInput(val) { let prompt = `Check the sentiment of this input and return a one word result:${val}`; try { this.result = 'Sentiment analysis: ' + await this.session.prompt(prompt); } catch(e) { console.log('window.ai pooped the bed', e); } } }))});</code></pre><p>Basically, I just have a hard-coded prompt where your input is appended and sent to the built-in model. I realize most folks can't test this yet, so here's a screenshot of this in action:</p><p><img src="https://static.raymondcamden.com/images/2024/08/sent1.jpg" alt="Input was 'I really dislike my body and have a pretty bad self image. I recognize that, which is good, but it doesn't necessarily stop the feelings I have. I'm too fat. I'm too clumsy. I'm too tall. All of these I know aren't important to my wife and kids, but I still struggle with it every day.' and the result is Dislike" class="imgborder imgcenter" loading="lazy"></p><p>And here's a more positive result:</p><p><img src="https://static.raymondcamden.com/images/2024/08/sent2.jpg" alt="Input was 'about a year ago my wife bought some simple crystals to hang in our house, just for fun, and i still remember the day i walked into the kitchen and saw rainbows. it was such a small thing that made me smile in a huge way. to this day, i love walking in the room and seeing random rainbows everywhere.' and the result is Joy" class="imgborder imgcenter" loading="lazy"></p><p>Obviously, those inputs were just tests, but you could imagine a customer support response form perhaps, or just a forum post in general using this to provide feedback. I'd like to see the browser API mimic Gemini's where you can use system instructions and JSON schema to 'force' a particular response type. You can find the complete demo below:</p><p class="codepen" data-height="500" data-default-tab="js,result" data-slug-hash="wvLpYoy" data-pen-title="window.ai test (sentiment analysis)" data-user="cfjedimaster" style="height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/cfjedimaster/pen/wvLpYoy"> window.ai test (sentiment analysis)</a> by Raymond Camden (<a href="https://codepen.io/cfjedimaster">@cfjedimaster</a>) on <a href="https://codepen.io">CodePen</a>.</span></p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script><p>So, not even close to being really ready for production, but what do you think?</p>
</content>
<category term="generative ai" />
<category term="javascript" />
<category term="development" />
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
</entry>
<entry>
<id>https://www.raymondcamden.com/2024/08/18/links-for-you</id>
<title>Links For You (8/18/2024)</title>
<updated>2024-08-18T18:00:00+00:00</updated>
<link href="https://www.raymondcamden.com/2024/08/18/links-for-you" rel="alternate" type="text/html" title="Links For You (8/18/2024)"/>
<content type="html">
<p>Woot, welcome to football season! Ok, preseason, but I am beyond excited to watch my Saints play later today and, hopefully, look halfway decent. My wife and I are both big football fans and the start of the season is always a happy occasion. This past week I traveled to Atlanta to give a lab on Adobe's Firefly Services APIs which went well, and I get to do it again next week in New York City. I hope your week goes well, and here are some interesting links for you to enjoy.</p><h2 id="migrating-from-netlify-to-cloudflare" tabindex="-1"><a class="header-anchor" href="#migrating-from-netlify-to-cloudflare">Migrating from Netlify to Cloudflare</a></h2><p>In this post, <a href="https://sia.codes/">Sia Karamalegos</a> discusses how she migrated her site <a href="https://sia.codes/posts/migrating-netlify-to-cloudflare/">from Netlify to Cloudflare</a>. This was done primarily due to Cloudflare releasing a new service to <a href="https://blog.cloudflare.com/declaring-your-aindependence-block-ai-bots-scrapers-and-crawlers-with-a-single-click">block AI crawlers</a>, something she felt important enough to go through the work of a migration. <a href="https://www.cloudflare.com/">Cloudflare</a> is pretty impressive and I've really dug their serverless support, and while I have no plans of migrating my site, Sia's guide is well done and could be helpful to others.</p><h2 id="pipedream-adds-if%2Felse" tabindex="-1"><a class="header-anchor" href="#pipedream-adds-if%2Felse">Pipedream Adds If/Else</a></h2><p>Readers know I've been a <strong>huge</strong> fan of <a href="https://pipedream.com">Pipedream</a> for years now. They are, to me, the best Low Code solution out there. One aspect they were missing though was more advanced workflow controls like IF/ELSE conditions. Well, now it's here, well, almost here. The feature is available for testing but not quite done yet. You can find out more and see a cool demo below:</p><lite-youtube videoid="vsgdBV3LG4E" style="background-image: url('https://i.ytimg.com/vi/vsgdBV3LG4E/hqdefault.jpg');"> <a href="https://youtube.com/watch?v=vsgdBV3LG4E" class="lty-playbtn" title="Play Video"> <span class="lyt-visually-hidden">Play Video</span> </a></lite-youtube><script defer src="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.css" integrity="sha512-utq8YFW0J2abvPCECXM0zfICnIVpbEpW4lI5gl01cdJu+Ct3W6GQMszVITXMtBLJunnaTp6bbzk5pheKX2XuXQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /><p><p>Personally, I really like how they implemented this, especially the support for collecting the results at the end of each branch.</p><h2 id="handling-rounding-errors-in-javascript" tabindex="-1"><a class="header-anchor" href="#handling-rounding-errors-in-javascript">Handling Rounding Errors in JavaScript</a></h2><p>For your last link, here is a deep look at <a href="https://www.robinwieruch.de/javascript-rounding-errors/">handling rounding errors</a> in JavaScript for financial applications. This isn't something I've ever had to worry about and always figured I'd research it when I need to, but I'm really happy I read this article as it was fascinating.</p><h2 id="and-now-for-something-completely-different" tabindex="-1"><a class="header-anchor" href="#and-now-for-something-completely-different">And Now For Something Completely Different</a></h2><p>This is - easily - the most silliest and interesting form I've seen. I don't want to say anymore as it's just fun to experience it:</p><p><a href="https://codepen.io/ksenia-k/pen/xxoqXbJ">https://codepen.io/ksenia-k/pen/xxoqXbJ</a></p><p>Thank you to the excellent <a href="https://frontendfoc.us/">Frontend Focus</a> mailing list for sharing this link first.</p>
</content>
<category term="links4you" />
<category term="misc" />
<author>
<name>Raymond Camden</name>
<email>raymondcamden@gmail.com</email>
</author>
</entry>
</feed>
If you would like to create a banner that links to this page (i.e. this validation result), do the following:
Download the "valid Atom 1.0" banner.
Upload the image to your own server. (This step is important. Please do not link directly to the image on this server.)
Add this HTML to your page (change the image src
attribute if necessary):
If you would like to create a text link instead, here is the URL you can use:
http://www.feedvalidator.org/check.cgi?url=https%3A//www.raymondcamden.com/feed.xml