CfA Spectrum Lab at https://datalabs.cfa.harvard.edu/spectrumlab/ .
The development preview deployment is at https://smithsonian.github.io/si_website_spectrum_lab .
- Node 22.9.0 or similar
If you need several Node versions to coexist on your machine, this project has a .tool-versions for use with mise (originally added for asdf).
- Prettier VS Code extension (recommended)
- Vue VS Code extension (recommended)
- Clone the repo.
- Inside the clone:
npm install
npm run dev
The app is now running at http://localhost:5173/ by default. Code changes will take effect immediately.
- Make changes to the metadata Google Sheet from the Smithsonian DataLabs Google Drive. It's called "Metadata Entry".
- Download it as a CSV. File -> Download -> Comma Separated Values (.csv)
- Put it next to
src/metadata.jsonand name itmetadata.csv. - Run this, from the repository root. If it worked, you'll see a bunch of Prettier log messages.
tools/import_metadatanpm run formatto apply code style. This is not necessary if your IDE formats with Prettier on save.npm run buildto verify build success. TypeScript errors may appear here that do not appear in development mode.- (Optional)
npm run previewafter building. This will serve the build output on 4173, so you can verify it looks right. This is overkill unless you're making changes to the build process.
- (Optional)
- Commit to the
mainbranch. - Push the
mainbranch. After a few minutes your changes will appear at https://smithsonian.github.io/si_website_spectrum_lab .
- Deploy to GitHub first.
- Use the
deploy_scriptsrepo to pull and deploy themainbranch.- Alternatively, use rsync or similar to apply the
makesteps manually.
- Alternatively, use rsync or similar to apply the
This is by far the most complex of the Data Labs Vue-based repos, with 112 distinct pages as of 2026-06-02.
Like most of the other Data Labs repos, this one is built on:
- SPA library: Vue 3
- Builder/bundler: Vite 6
- Source language: TypeScript
- Output type: static pre-generation using Vite-SSG
- CSS framework: Bootstrap 5
Vue is a popular SPA (single-page application) framework, similar in concept to React or Angular. We chose it to ease collaboration with the Cosmic Data Stories team, which was already using Vue for their projects.
Vue can be written with several styles, in our case we chose Single-File Components to keep HTML, JS, and CSS co-located for a given component. These are the .vue files that make up the majority of the project. We also chose the more recently-introduced and more flexible Composition API over the older Options API.
The routes are set up using vue-router. In typical SPA fashion, this allows users to navigate between pages without a server roundtrip, significantly speeding up navigation while maintaining the familiar browser idioms of clicking on links and using the Back button when necessary.
However, having a single simple HTML page with all the content rendered by JS, as is traditional for SPAs, introduces some problems with accessibility, performance, and SEO. We chose to use Vite-SSG to pre-render each page so it can be viewed and understood at least partially in HTML, before the JS is available. This also simplifies the Apache config, which can operate relatively normally, rather than needing special rules to route everything to the index.html.
We do still use a .htaccess rewrite rule to ensure each link can find its corresponding HTML file.
The routes themselves are configured in main.ts in a single hierarchical object. When editing this object, ensure there is a trailing slash in paths that have sub-pages, or the corresponding URLs and index.htmls may not be generated correctly.
Note that the createApp function is generated by calling ViteSSG instead of using the Vue createApp function. The BASE_URL is also key. It is set at build time to build different bundles with different URLs for the different environments of local, staging, and prod.
The bread and butter of SPAs. Vue has sophisticated tools for using component state to change what's rendered, and vice versa. This, plus the "prop" concept of components, allowed us to cut up the spectrum tool into small reusable pieces that could be assembled in different ways to handle all the different situations throughout the Spectrum Lab.
Some Vue apps use Pinia to keep track of state that's global to the application. We didn't find any use for it here, since we always want the spectrum tool's state to be reset when moving to a new page. So all the state is handled at the component level.
However, there were many places where we needed state to be managed at a high level but available many layers down the hierarchy. For example, the rainbow and the chart for a given tool both need access to the same spectrum data. And the mouseover measuring tool needs to have the same shared value between both the top and bottom tools on a page.
For this we used Vue's provide/inject functions. When the provided value is only set from the top down, we use provide and inject directly.
However, there were also many situations where the lower-down components need to update the higher value in a shared way. For example, the spectrum data can be set by the dropdown, or an imported file, or by drawing, and needs to be propagated correctly to both the graph and the rainbow. The controls, and graph, and measuring tool, also need to know whether or not we're in drawing mode.
In these cases, Vue recommends injecting both the value and a setter, and never writing to the injected value directly from a lower component. We used the composable concept to ensure this pattern was followed. The highest-level component to use a given composable controls the state, and any lower components will use the provided read-only state and one or more functions for changing that state. Basically, a state machine. The resulting TypeScript is a bit maddening, you can see the general pattern copy-pasted in many of the files under utils. I tried abstracting it out once and gave up, it was more trouble than it was worth. But the resulting composables have very nice strict control over how their underlying state machines operate.
The hardest area of the code to manage with sanity was the ToolCard itself. I ended up moving as much logic as I could into the toolCardUtils to keep the file from ballooning out of control. Many of the composables called in its setup function have their output fed into later composables, which themselves are fed into still later ones, but hopefully that makes it as clear as possible which template areas are dependent on which state.
TODO
We use TypeScript for its autocomplete abilities and its assurance of type correctness within code. It can be a bit ornery and I (Vanessa) wouldn't hold it against someone who chose to get rid of it. But I find it helpful.
Bootstrap 5 is a great library, but it is a bit old-fashioned, and its dependency on deprecated SASS features limits us to Vite 6. This issue tracked this fix for Bootstrap 6 which isn't out yet. twbs/bootstrap#29853 If we ever migrate to Bootstrap 6, we could then upgrade Vite, but as long as Vite 6 remains supported, it's probably not worth the trouble.
Bootstrap theme customization can be found here. Like the other Data Labs sites, we set Laura's colors using SASS and CSS variables, for use in Bootstrap's code, as well as CSS elsewhere in the site.
This repo in particular has significant SASS variable customization on the Bootstrap buttons, inputs, and form controls, to get the spectrum tool looking correct according to Laura's mocks.
Bootstrap components are usually done with the bootstrap-vue-next library, which allows us to use opinionated TypeScript autocomplete rather than writing the classes by hand, as well as ensuring Bootstrap's JS plays nicely with Vue's. Occasionally we stick with the ordinary Bootstrap classes, when the Vue components are overkill, or when tighter control is needed.