Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

User guide

Creating a new application

Lens is framework agnostic, so there are many ways to build and deploy your application. This guide focuses on compatibility with other projects in the Samply organization, so we use SvelteKit as the frontend framework and Docker for deployment.

To create a new SvelteKit application run npx sv create my-app. Use the minimal template with TypeScript syntax and select Prettier and ESLint when prompted.

Now cd to the new directory and install Lens:

npm install @samply/lens

Prettier config

The Prettier config created by sv create uses tabs and sets the print width to 100 against the recommendation of Prettier. We recommend to remove these options from .prettierrc and use the Prettier defaults with the Svelte plugin only:

{
    "plugins": ["prettier-plugin-svelte"],
    "overrides": [
        {
            "files": "*.svelte",
            "options": {
                "parser": "svelte"
            }
        }
    ]
}

Configuring the root route

Typically your application will only use the root route at src/routes. We will import the Lens CSS and JS bundles and render the main application component. Because Lens uses Web Components we need to disable HMR and SSR. Change the content of src/routes/+page.svelte to:

<script>
    // Using hot module replacement (HMR) with custom elements (aka web
    // components) does not work because a custom element cannot be updated once
    // registered, see https://github.com/WICG/webcomponents/issues/820.
    // Therefore we do a full page reload instead of HMR.
    if (import.meta.hot) {
        import.meta.hot.on('vite:beforeUpdate', () => {
            window.location.reload();
        });
    }

    // Import Lens CSS and JS bundles
    import "@samply/lens/style.css";
    import "@samply/lens";

    import App from '../App.svelte';
</script>

<App />

And add src/routes/+page.ts:

// Using server-side rendering (SSR) with custom elements (aka web components)
// causes issues because the server does not know that an element is a Svelte
// component and converts all props to strings.
export const ssr = false;

The application component

Your main application code lives in the application component. Create the file src/app.css and leave it empty and create src/App.svelte with the following content:

<script lang="ts">
    import "./app.css";
</script>

<lens-search-button></lens-search-button>

Now run npm run dev and open http://localhost:5173/ in your browser. You should see a search button in the top left corner of the page.

Options and catalogue

Your application must pass two objects to Lens. The LensOptions object contains general configuration options and the Catalogue object describes what users can search for. You can define these objects in TypeScript but many applications in the Samply organization define them in JSON files.

Assuming you are using JSON files, create the file src/options.json containing the empty object {} and the file src/catalogue.json with the following content:

[
    {
        "key": "rh_factor",
        "name": "Rh factor",
        "system": "",
        "fieldType": "single-select",
        "type": "EQUALS",
        "criteria": [
            {
                "key": "rh_positive",
                "name": "Rh+"
            },
            {
                "key": "rh_negative",
                "name": "Rh-"
            }
        ]
    }
]

Add the following to the top of src/App.svelte to load the JSON files and pass the objects to Lens:

<script lang="ts">
    import { onMount } from "svelte";
    import {
        setOptions,
        setCatalogue,
        type LensOptions,
        type Catalogue,
    } from "@samply/lens";
    import options from "./options.json";
    import catalogue from "./catalogue.json";
    onMount(() => {
        setOptions(options as LensOptions);
        setCatalogue(catalogue as Catalogue);
    });
</script>

<lens-catalogue></lens-catalogue>

When you run npm run dev you should see the catalogue component with the "Rh factor" item.

Schema validation

Lens includes JSON schema definitions for the options and the catalogue type. Create the script scripts/validate-json-schema.bash to validate your JSON files against the schema definitions:

set -e # Return non-zero exit status if one of the validations fails
npx ajv validate -c ajv-formats -s node_modules/@samply/lens/schema/options.schema.json -d src/options.json
npx ajv validate -c ajv-formats -s node_modules/@samply/lens/schema/catalogue.schema.json -d src/catalogue.json

Then install the required dependencies and test the script:

npm install ajv-cli ajv-formats --save-dev
bash scripts/validate-json-schema.bash

You can also configure VS Code to validate your JSON files against the schema definitions. This will show validation errors in your editor and provide IntelliSense. To do so add the following configuration to your workspace settings in VS Code:

"json.schemas": [
    {
        "fileMatch": [
            "catalogue*.json"
        ],
        "url": "./node_modules/@samply/lens/schema/catalogue.schema.json",
    },
        {
        "fileMatch": [
            "options*.json"
        ],
        "url": "./node_modules/@samply/lens/schema/options.schema.json",
    },
]

Test environment

It is a common requirement to load different options in test and production. You can achieve this by using a feature of SvelteKit that makes environment variables from the server available in the browser. Applications in the Samply organization commonly accept the following environment variables:

  • PUBLIC_ENVIRONMENT: Accepts the name of the environment, e.g. production or test
  • PUBLIC_BACKEND_URL: Overwrites the URL of the backend that your application queries

For example you could handle the PUBLIC_ENVIRONMENT variable as follows:

<script lang="ts">
       import { env } from "$env/dynamic/public";
       ...
    onMount(() => {
           setOptions(env.PUBLIC_ENVIRONMENT === "test" ? testOptions : prodOptions);
    });
       ...
</script>

Deployment

We recommend that projects in the Samply organization follow these deployment practices. We will use Node.js inside Docker. Run npm install @sveltejs/adapter-node and change the adapter in svelte.config.js:

-import adapter from '@sveltejs/adapter-auto';
+import adapter from '@sveltejs/adapter-node';

Then you can remove @sveltejs/adapter-auto from package.json. Now create a Dockerfile with the following content:

FROM node:22-alpine AS builder
WORKDIR /app

# Install dependencies first to leverage Docker cache
COPY package.json package-lock.json ./
RUN npm ci

# Copy the rest of the application
COPY vite.config.ts svelte.config.js ./
COPY src ./src
COPY static ./static

# Build the SvelteKit project
RUN npm run build

# Production image
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/build ./build
EXPOSE 3000
CMD ["node", "build"]

To automatically build Docker images and publish them to Docker Hub when a branch changes, we recommend to use the Samply Docker CI workflow for GitHub Actions. Use the workflow template or copy the following into .github/workflows/docker.yml:

# This workflow builds a Docker image from the Dockerfile and publishes the
# image to Docker Hub. How the image tags are chosen is documented here:
# https://github.com/samply/github-workflows/blob/main/.github/workflows/docker-ci.yml
#
# This file is copied and adapted from:
# https://github.com/samply/.github/blob/main/workflow-templates/docker-ci-template.yml

name: Docker CI

on:
    push:
        branches:
            - main
            - develop
        # Build when a new version is tagged
        tags:
            - "v*.*.*"
    pull_request:
        branches:
            - main
            - develop
jobs:
    build:
        # This workflow defines how a samply docker image is built, tested and published.
        # Visit: https://github.com/samply/github-workflows/blob/main/.github/workflows/docker-ci.yml, for more information
        uses: samply/github-workflows/.github/workflows/docker-ci.yml@main
        with:
            # The Docker Hub Repository you want eventually push to, e.g samply/share-client
            image-name: "samply/your-project"
            # Where to push your images ("dockerhub", "ghcr", "both" or "none")
            push-to: dockerhub
        # This passes the secrets from calling workflow to the called workflow
        secrets:
            DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
            DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}

Linting in GitHub Actions

You can use GitHub Actions to run the following checks on pull requests:

  • svelte-check to check for TypeScript compiler errors
  • Prettier
  • ESLint
  • Test that the build works
  • Validate catalogue and options

To do so create .github/workflows/linting.yml with the following content:

name: Linting
on:
    pull_request:
        branches:
            - main
            - develop
    push:
        branches:
            - develop

jobs:
    verify-code:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
            - run: npm ci
            - run: npx prettier --check .
            - run: npx eslint .
            - run: npm run check
            - run: npm run build
            - run: bash scripts/validate-json-schema.bash

Overwriting styles

All Lens components have a unique part attribute that you can use in your application to overwrite styles. We try our best to keep the names of part attributes stable but keep in mind that sometimes we will have to change the structure of a component which will likely break your style overrides.

For example overwrite the color of the search button as follows:

::part(lens-search-button) {
    background-color: red;
}
::part(lens-search-button):hover {
    background-color: salmon;
}

Query

In samply/lens, there are three important structures for building a query:

  • Catalogue
  • Query Data
  • Lens-AST (Abstract Syntax Tree)

Catalogue

The Catalogue contains all possible query elements used in your search or exploration application. Lens expects a catalogue to be provided during initialization — even an empty one is valid.

The catalogue can either be:

  • a local file included in your project, or
  • fetched dynamically via a REST call.

⚠️Important: While it is technically possible to retrieve and modify the catalogue at runtime, this is not recommended.

The structure of the catalogue is defined in schema and type. Valdiating your catalogue can be done within VS Code with the schema, see here.

Subgroups

The catalogue supports the definition of subgroups. For example, you might group all patients with diabetes at the top level, while also distinguishing between different types of diabetes. If a user wants to find patients with any form of diabetes, this can be expressed using subgroups in the catalogue.

Subgroups allow you to structure complex concepts in a way that supports both broad and narrow search criteria.

/**
 * Fetches the catalogue and options file from the given URLs.
 * @param catalogueUrl The URL of the catalogue.
 * @param optionsUrl The URL or path of the options file.
 * @returns A promise that resolves to an object containing the catalogue and options as JSON strings
 */
export const fetchData = async (
    catalogueUrl: string,
    optionsUrl: string,
): Promise<{ catalogueJSON: string; optionsJSON: string }> => {
    const cataloguePromise: string = await fetch(catalogueUrl).then(
        (response) => response.text(),
    );

    const optionsPromise: string = await fetch(optionsUrl).then((response) =>
        response.text(),
    );

    return Promise.all([cataloguePromise, optionsPromise]).then(
        ([catalogueJSON, optionsJSON]) => {
            return { catalogueJSON, optionsJSON };
        },
    );
};

Svelte integration

If you're using Svelte, we recommend starting with this structure:

const jsonPromises: Promise<{
    catalogueJSON: string;
    optionsJSON: string;
}> = fetchData(catalogueUrl, optionsFilePath);
{#await jsonPromises}
    <p>Loading data...</p>
{:then { optionsJSON, catalogueJSON }}
    <lens-options {catalogueJSON} {optionsJSON} {measures}></lens-options>
{:catch someError}
    System error: {someError.message}
{/await}

Query Data

Once a user selects an element from the catalogue, it is added to the query store. Like the catalogue, query elements can also be added programmatically using setQueryStoreAPI.

Query Data is the internal representation of the user's current query. It contains all necessary information required to construct the final query output.


Lens-AST

To allow external systems (e.g., databases or APIs) to understand the query, the internal Query Data is transformed into the Lens-AST.

AST stands for Abstract Syntax Tree. It represents the query in a structured, hierarchical format that is decoupled from the original catalogue.

The root of the AST is an types. It defines the overall logical structure using one of the following operators:

"AND" | "OR" | "XOR" | "NOT"

The children of an types can be either another types or an https://samply.github.io/lens/docs/types/AstBottomLayerValue.html. An AstBottomLayerValue contains the actual filter expressions — for example, gender = male.

Empty Query

Since Lens is designed for exploratory querying, it supports an empty query, which returns all available data. In this case, Lens generates the following AST:

{
    "operand": "OR",
    "children": []
}

AST Example

The AST types are located in types. Here's an example of a more complex query structure:

{
    "operand": "OR",
    "children": [
        {
            "operand": "AND",
            "children": [
                {
                    "key": "gender",
                    "operand": "OR",
                    "children": [
                        {
                            "key": "gender",
                            "type": "EQUALS",
                            "system": "",
                            "value": "male"
                        }
                    ]
                }
            ]
        }
    ]
}

This AST includes two nested AstTopLayer objects with OR and AND operators. The inner AstTopLayer contains a key, indicating that its children are logically grouped under this key — in this case, gender.

This layer provides context for the query at the database level. In the deepest children array, we see the actual condition: we are searching for patients whose gender is equal to "male".


Converting Query Data to AST

To send a query to a database or external service, you can subscribe to the query store to get the current state, then convert it to an AST using:

const ast = buildAstFromQueryStore(queryStore);

Handling Subgroups in AST

If your catalogue includes subgroups, we recommend expanding them in the query before processing. This can be done easily using:

const astWithSubCategories = resolveAstSubCategories(ast);

This function replaces subgroup references with their actual sub-elements, making the query explicit and ready for processing.

Translations

Lens supports English and German language out of the box. You can set the language to en or de in the Lens options:

"language": "de"

Overwriting texts

You can overwrite the built-in translations in the Lens options to customize the text:

"texts": {
    "loading": {
        "en": "Processing..."
    }
}

Or add translations for new languages:

"texts": {
    "loading": {
        "es": "Cargando..."
    }
}

Or you can add your own texts that you can then translate in your application. We recommend that you prefix the key with your project name to avoid collisions:

"texts": {
    "ccp-welcome": {
        "en": "Welcome to CCP Explorer!",
        "de": "Willkommen beim CCP Explorer!"
    }
}

Using translations in your application

You can also use the translations in your application:

<script lang="ts">
    import { translate } from "@samply/lens";
</script>

<span>{translate("loading")}</span>

Chart reset button

This is how you would implement a reset button using the resetDiagrams function.

<script>
    import { resetDiagrams } from "@samply/lens";
</script>

<!-- example for a simple button-->
<button class="reset-button" onclick="{resetDiagrams}">Reset</button>

<!-- styling to fit with the rest/other buttons -->
<style>
    button.reset-button {
        background-color: var(--button-background-color);
        color: var(--button-color);
        border: none;
        border-radius: var(--border-radius-small);
        padding: var(--gap-xs) var(--gap-s);
        font-size: var(--font-size-m);
        cursor: pointer;
        display: flex;
        align-items: center;
        gap: var(--gap-xs);
    }
</style>

Facet counts

Lens can query facet counts from a backend and display them in the catalogue. Facet counts are roughly speaking the number of results one would get when only searching for that criteria.

Example of facet counts

To enable facet counts add the following options:

"facetCount": {
    "backendUrl": "http://localhost:5124/prism",
    "hoverText": {
        "gender": "Matching patients for this criterion only",
        "diagnosis": "Total number of this diagnosis across all patients",
        "sample_kind": "Matching samples for this criterion only"
    }
},

hoverText controls the text that is displayed when hovering the mouse over the number chips.

Lens POSTs an array of sites (e.g. {"sites": ["berlin", "munich"]}) to the endpoint and expects facet counts in the following format:

{
    "diagnosis": {
        "C34.0": 26,
        "C34.2": 28,
        "C34.8": 25
    },
    "gender": {
        "female": 31,
        "male": 43
    }
}

Development

Git hooks

The repository uses Husky with lint-staged to lint your commits. Husky automatically sets up a git hook when you do npm install for the first time. When you git commit the commit message is checked to comply with Conventional Commits and your staged files (and staged files only) are verified with ESLint and formatted with Prettier. If there are any problems you will get an error at the time of commit. This has the advantage that you notice problems before creating a pull request and without having to wait for GitHub Actions (our CI tool) to complete.

Styling

Lens tries to be very customizable and thus all built-in styles should be overridable by applications. Styles in web components are usually scoped to the component and cannot be overwritten from the outside. Therefore all HTML elements in Lens should use the part attribute:

<button part="lens-search-button"></button>

This enables applications to override styles from the outside as follows:

::part(lens-search-button) {
    background-color: red;
}

However the ::part(foobar) selector does not work when used in the .svelte component file itself. Therefore the convention in Lens is to use the [part~="foobar"] selector inside components:

[part~="lens-search-button"] {
    background-color: blue;
}

This has several advantages:

  • We can scope our CSS styles to the component which eases maintainability
  • We don't have to come up with and specify an additional class name
  • We cannot forget to set part attributes because we use them for ourselves for styling

Note that the old convention in Lens was to keep styles in a CSS file separate from the .svelte component. Many components still use the old convention but we are moving towards component scoped styles.

Making a release

The new version number must follow semantic versioning and look like X.Y.Z. To make a new release follow these steps:

  1. Create or update the release notes at book/src/releases/vX.Y.Z.md and make a PR to merge them into develop. The release notes should include a migration guide if there are breaking changes.
  2. Run npm run version X.Y.Z and make a PR to merge the resulting version bump commit into develop.
  3. Make a PR to merge develop into main.
  4. Verify that the CI successfully builds and releases to npm and creates a release on GitHub.

Release notes

Version 0.5.3

New features

  • Exported addItemToActiveQueryGroup to allow applications to programmatically add filters to the search bar

Migration guide

No breaking changes.

Version 0.5.2

New features

  • Added an optional page size switcher to the result table
  • Added facet counts to the catalogue
  • Adjusted appearance of the autocomplete input to be consistent with other catalogue inputs

Migration guide

No breaking changes.

Version 0.5.1

Migration guide

We have changed the internal structure of most catalogue items. If your application overrides catalogue item styles you will likely have to adjust your styles.

Version 0.5.0

Lens version 0.5.0 paves the way for better documentation of the Lens library with the introduction of the Lens Book and API docs. Version 0.5.0 also introduces the translations API and the setOptions and setCatalogue APIs that pose an alternative to the <lens-options></lens-options> component. Finally it comes with internal improvements such as the migration to Svelte 5 and the automatic generation of the catalogue JSON schema to keep it in sync with the TypeScript definitions. You can find the full list of changes on GitHub.

Migration guide

Version 0.5.0 introduces a few breaking changes that application authors should be aware of. This guide will help you to migrate your application to the new version.

CSS bundle import path

The import path of the CSS bundle has changed. If the import is processed by your bundler we recommend to use automatic module resolution:

import "@samply/lens/style.css";

If you have to refer to the file directly you can find it at node_modules/@samply/lens/dist/lens.css.

Catalogue changes

The catalogue JSON schema is a little bit stricter now. Most notably every object with the "childCategories" property now additionally requires "fieldType": "group". For example:

 {
+    "fieldType": "group",
     "key": "donor",
     "name": "Donor/Clinical Information",
     "childCategories": [
         ...
     ]
 }

You can use the following this script to update your catalogue. Afterwards open the file in your editor and reformat it.

sed -i 's/"childCategories"/"fieldType": "group", "childCategories"/' catalogue.json

Catalogue icons

Icons have been removed from the <lens-catalogue> component:

 <lens-catalogue
-    toggleIconUrl="right-arrow-svgrepo-com.svg"
-    addIconUrl="long-right-arrow-svgrepo-com.svg"
-    infoIconUrl="info-circle-svgrepo-com.svg"
     ...
 ></lens-catalogue>

Add them to the iconOptions object in the Lens options instead. Also remove the selectAll property and use the new translations API instead if you want to customize the text.

 "iconOptions": {
     "deleteUrl": "delete_icon.svg",
     "infoUrl": "info-circle-svgrepo-com.svg",
+    "toggleIconUrl": "right-arrow-svgrepo-com.svg",
+    "addIconUrl": "long-right-arrow-svgrepo-com.svg",
-    "selectAll": {
-        "text": "Alle Hinzufügen"
-    }
 }

Schema validation

JSON schema definitions for the Lens options and the catalogue are now included in the Lens release. Refer to this section in the new application guide to learn how to validate your JSON files against the JSON schema.