Nerdy Creativity

CSS Nine Years Later

I updated the styles on a site that I hadn't updated since October 2015, over nine years ago.

The old code had floats to create two column layout.

#content {
  float: left;
  width: 600px;
}

#menu {
  margin-left: 620px;
}

The new code uses CSS grid.

#wrapper {
  display: grid;
  grid-template-columns: 6fr 4fr;
}

Despite being 9 years old, the javascript and css still worked. That's the advantage of javascript and css - backwards compatibility.

The site is written in javascript and css. I didn't have to deal with complicated build processes and updating libraries to start and update the site. The 3rd party javascript libraries and css were in the project directories. Sometimes using a simple solution pays off in the long term.


Adding PDF viewer to Eleventy

I wanted to add a PDF viewer to Eleventy using PDF.js.

At first, I installed PDF.js from npm. However, the npm version only works on Firefox and Chrome. To get a version that works on Safari, I needed to use the legacy build version.

I cloned PDF.js repo and installed dependencies.

git clone https://github.com/mozilla/pdf.js.git
cd pdf.js
npm install

Then I built the legacy version.

npx gulp generic-legacy

This generated pdf.js and pdf.worker.js in the build/generic-legacy/build/ directory. I copied pdf.js and pdf.worker.js to /public/lib/pdfjs/generic-legacy/build.

Then I put the code to create a viewer in public/lib/pdfviewer.js. The code is based on PDF.js Prev/Next example and mobile viewer example.

// set workerSrc property
pdfjsLib.GlobalWorkerOptions.workerSrc =
  "/lib/pdfjs/generic-legacy/build/pdf.worker.mjs";

let pdfDoc = null;
let pageNum = 1;
let pageRendering = false;
let pageNumPending = null;
let scale = 1;
let canvas = document.getElementById("the-canvas");
let ctx = canvas.getContext("2d");
const DEFAULT_SCALE_DELTA = 1.1;
const MIN_SCALE = 0.25;
const MAX_SCALE = 10.0;

/**
 * Get page info from document, resize canvas accordingly, and render page.
 */
function renderPage(num) {
  if (canvas == null) return;

  pageRendering = true;
  // Using promise to fetch the page
  pdfDoc.getPage(num).then(function (page) {
    var viewport = page.getViewport({ scale: scale });
    // Support HiDPI-screens.
    var outputScale = window.devicePixelRatio || 1;

    canvas.width = Math.floor(viewport.width * outputScale);
    canvas.height = Math.floor(viewport.height * outputScale);
    canvas.style.width = Math.floor(viewport.width) + "px";
    canvas.style.height = Math.floor(viewport.height) + "px";

    var transform =
      outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null;

    // Render PDF page into canvas context
    var renderContext = {
      canvasContext: ctx,
      transform: transform,
      viewport: viewport,
    };
    var renderTask = page.render(renderContext);

    // Wait for rendering to finish
    renderTask.promise.then(function () {
      pageRendering = false;
      if (pageNumPending !== null) {
        // New page rendering is pending
        renderPage(pageNumPending);
        pageNumPending = null;
      }
    });
  });

  // Update page counters
  document.getElementById("page_num").textContent = num;
}

/**
 * If another page rendering in progress, waits until the rendering is
 * finished. Otherwise, executes rendering immediately.
 */
function queueRenderPage(num) {
  if (pageRendering) {
    pageNumPending = num;
  } else {
    renderPage(num);
  }
}

/**
 * Displays previous page.
 */
function onPrevPage() {
  if (pageNum <= 1) {
    return;
  }
  pageNum--;
  queueRenderPage(pageNum);
}

/**
 * Displays next page.
 */
function onNextPage() {
  if (pageNum >= pdfDoc.numPages) {
    return;
  }
  pageNum++;
  queueRenderPage(pageNum);
}

/**
 * Zoom in
 */
function pdfViewZoomIn() {
  let newScale = scale;
  newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2);
  newScale = Math.ceil(newScale * 10) / 10;
  newScale = Math.min(MAX_SCALE, newScale);
  scale = newScale;

  queueRenderPage(pageNum);
}

/**
 * Zoom out
 */
function pdfViewZoomOut() {
  let newScale = scale;
  newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2);
  newScale = Math.floor(newScale * 10) / 10;
  newScale = Math.max(MIN_SCALE, newScale);
  scale = newScale;

  queueRenderPage(pageNum);
}

/**
 * Setup event handlers
 */
function setupEventHandlers() {
  let prevEl = document.getElementById("prev");
  if (prevEl) prevEl.addEventListener("click", onPrevPage);

  let nextEl = document.getElementById("next");
  if (nextEl) nextEl.addEventListener("click", onNextPage);

  let zoomInEl = document.getElementById("zoomIn");
  if (zoomInEl) zoomInEl.addEventListener("click", pdfViewZoomIn);

  let zoomOutEl = document.getElementById("zoomOut");
  if (zoomOutEl) zoomOutEl.addEventListener("click", pdfViewZoomOut);
}

export async function init(url) {
  setupEventHandlers();

  // download pdf
  var loadingTask = pdfjsLib.getDocument(url);
  pdfDoc = await loadingTask.promise;

  // set page count
  let countEl = document.getElementById("page_count");
  if (countEl) countEl.textContent = pdfDoc.numPages;

  // render first page
  renderPage(pageNum);
}

I added a template file in _includes/pdfviewer.njk. pdfviewer.njk calls the init() from pdfviewer.js with pdfUrl and pdfScale arguments.


<style>
  {% include "css/pdfviewer.css" %}
</style>

<section class="pdfviewer">
  <div>
    <button id="prev" type="button" title="previous page">Previous</button>
    <button id="next" type="button" title="next page">Next</button>
    <button id="zoomIn" type="button" title="zoom in">+</button>
    <button id="zoomOut" type="button" title="zoom out">-</button>
    <span
      >Page: <span id="page_num"></span> / <span id="page_count"></span
    ></span>
  </div>

  <canvas id="the-canvas" style="border: 1px solid black;"></canvas>
</section>

<script src="/lib/pdfjs/generic-legacy/build/pdf.mjs" type="module"></script>
<script type="module">
  import {init} from '/pdfviewer.js'
  init('{{pdfUrl}}', {{pdfScale}})
</script>

Then in the file I want to add a pdf, I included pdfviewer.njk and passed in the pdfUrl.


{% set pdfUrl = '/assets/pdfs/pcc-portfolio/inat_la_river.pdf' %}
{% include "pdfviewer.njk" %}

To adjust the size of the pdf, I passed in pdfScale argument. If it is not set, the value defaults to 1.


{% set pdfUrl = '/assets/pdfs/pcc-portfolio/inat_la_river.pdf' %}
{% set pdfScale = '.4' %}
{% include "pdfviewer.njk" %}

Here's a PDF.

Page: /


Adding Leaflet maps to Eleventy

I wanted to find out how to add Leaflet maps to Eleventy, and I came across this post from Mike Neumegen at cloudcannon. The code is kinda outdated, so I had to update some things.

First you create in locations.json file in the _data folder. locations.json is an array of locations

[
  {
    "name": "Kentucky Ridge State Forest",
    "latitude": "36.736700",
    "longitude": "-83.762480"
  },
  {
    "name": "Amity Park",
    "latitude": "35.932640",
    "longitude": "-82.006000"
  },
  {
    "name": "Mill Creek Park",
    "latitude": "40.030819",
    "longitude": "-122.115387"
  },
  {
    "name": "Willamette National Forest",
    "latitude": "44.058990",
    "longitude": "-122.484970"
  },
  {
    "name": "The Mound",
    "latitude": "32.490819",
    "longitude": "-80.320408"
  }
]

Then you add a file called map.njk in the _includes folder. map.njk has CDN links for Leaflet, html markup for the map, and the javacript code to create a Leaflet map.


<link
  rel="stylesheet"
  href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
  integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
  crossorigin=""
/>
<!-- Make sure you put this AFTER Leaflet's CSS -->
<script
  src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
  integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
  crossorigin=""
></script>

<div id="map"></div>

<style>
  #map {
    height: 300px;
  }
</style>

<script>
  // https://mozilla.github.io/nunjucks/templating.html#builtin-filters
  // https://github.com/11ty/eleventy/issues/1158

  // dump(2) will JSON.stringify markers object with 2 spaces.
  // safe will ensure that the data is not escaped.
  let markers = {{ markers | dump(2) | safe }}

  // create map
  const map = L.map('map');

  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
      {attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'})
      .addTo(map);

  // add location markers to map
  let bounds = [];
  for (let i = 0; i < markers.length; i++ ) {
      const marker = L.marker([markers[i].latitude, markers[i].longitude]).addTo(map);
      marker.bindPopup(markers[i].name);
      bounds.push([markers[i].latitude, markers[i].longitude]);
  }

  // adjust the map to show all location markers
  map.fitBounds(bounds);
</script>

Then in the file you want to add the map, include map.njk and pass in the markers data and the height of the map.


{% set markers = locations %}
{% set mapHeight = '400px' %}
{% include "map.njk" %}

Here's the map with markers.


Communicating Information - ArcGIS Dashboard vs ArcGIS StoryMap

For the final project of the Pasadena City College GEOG 115 Environmental Analysis with GIS class, students had the choice of creating a ArcGIS Dashboard or creating a ArcGIS StoryMap. Two students including myself choose to do dashboards, 18 students choose to do StoryMaps.

GIS is a technical subject. People are dealing with creating maps and charts to analyze an visualize geospatial data. Yet only two people were interested in creating dashboards to display that data. The rest wanted to tell a story with prose, images, and videos.

If storytelling is the preferred method of communicating for these geography students, then why do so many people in academic science research community deemphasized and discouraged science communication to the general public?


Day at UCLA Botany Gardens

I spent the day at UCLA Mathias Botanical Garden. I went on a guided tour of the garden, attended a plant propagation class, and attended a drawing plants class. I also got unexpectedly got a chance to hang out with some friends, one of whom works at the gardens.

During the drawing class, one of the attendees asked for some tips about how to get started. She and her boyfriend had no experience drawing. I would rate my drawing skills as above stick figure level, but nowhere good enough do it as a career. I gave her some tips, and she commended me on my advice. Turns out she's a teacher so she noticed I focused on growth mindset and positive reinforcement, which is crucial to creating a good learning environment.

My friend who works at the Natural History Museum of Los Angeles County told me that some of the high schools students who attended the R coding workshop earlier this year actually used some of what they learned in their school presentation.

It was nice getting positive feedback about my teaching attempts. Yeah, I'm a better at informal education than I am at sketching or identifying plants and animals, and that's totally cool with me.


5 more posts can be found in the archive.