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

This guide will help you set up your exploration tool using Lens.

We recommend starting with a new application using the new-app guide, followed by connecting your data source as described in the query documentation and passing results to Lens as shown in the showing results guide. Documentation for all available components

To customize the appearance of lens components, refer to the overwriting styles guide for instructions on how to apply your own styles.

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/config/options.json containing the empty object {} and the file src/config/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 "./config/options.json";
    import catalogue from "./config/catalogue.json";
    onMount(() => {
        setOptions(options as LensOptions);
        setCatalogue(catalogue as Catalogue);
    });
</script>

<lens-search-bar></lens-search-bar>
<lens-catalogue></lens-catalogue>

When you run npm run dev you should see the search bar and the catalogue component with the "Rh factor" entry. Open the "Rh factor" entry and click the plus icons next to Rh+ and Rh- in order to add them to the search bar.

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/config/options.json
npx ajv validate -c ajv-formats -s node_modules/@samply/lens/schema/catalogue.schema.json -d src/config/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 projects .vscode/settings.json:

"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_SPOT_URL: Overwrites the URL of the Spot backend that your application queries

For example you could handle the these variable as follows:

<script lang="ts">
    import type { LensOptions } from "@samply/lens";
    import { env } from "$env/dynamic/public";
    import optionsProd from "./config/options.json";
    import optionsTest from "./config/options-test.json";
    ...
    onMount(() => {
        let options: LensOptions = optionsProd;
        if (env.PUBLIC_ENVIRONMENT === 'test') {
            options = optionsTest;
        }
        if (env.PUBLIC_SPOT_URL) {
            options.spotUrl = env.PUBLIC_SPOT_URL;
        }
        setOptions(options);
    });
    ...
</script>

Reading the query and showing results

When the user clicks the search button, a typical application will read the current query from the search bar, send the query to some kind of backend to get results, and then pass the results to Lens so it can show them.

Add the following to src/App.svelte to print the current query to the console when the search button is clicked and show some hardcoded results in a pie chart. Of course, in a real application the results would depend on the query. For example a user might want to know the gender distribution of people who are Rh positive.

<script lang="ts">
    import { getAst, setSiteResult } from "@samply/lens";
    window.addEventListener("lens-search-triggered", () => {
        console.log("AST:", JSON.stringify(getAst()));

        setSiteResult("berlin", {
            totals: {},
            stratifiers: {
                gender: {
                    female: 9,
                    male: 3,
                },
            },
        });
    });
</script>

<lens-chart
    title="Gender distribution"
    catalogueGroupCode="gender"
    chartType="pie"
    displayLegends="{true}"
></lens-chart>

You can read more about queries and the AST and about showing results in the dedicated guides.

Deploying using Docker

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

The 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 structure of the catalogue is defined in schema and type. Validating 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.

async function fetchCatalogue() {
    const catalogue: Catalogue = await fetch(catalogueUrl).then((response) =>
        response.json(),
    );
    setCatalogue(catalogue);
}

Querying a backend

Lens has two representations of the query in the search bar: the query store and the AST.

Query store

Once a user selects an element from the catalogue, it is added to the query store. The query store is of type QueryItem[][], where a QueryItem represents a chip in the search bar and the outer list represents a logical OR operation and the inner list a logical AND operation. Take for example this query:

Example of search bar

The query searches for patients with blood group A- and a body weight between 30 and 100 as well as patients with blood group B+ regardless of their body weight. In the query store this query is represented as follows:

[
    [
        {
            "id": "10254884-b969-4bb2-8da9-d40eeb08586e",
            "key": "blood-group",
            "name": "Blood group",
            "type": "EQUALS",
            "system": "",
            "values": [
                {
                    "name": "A+",
                    "value": "A+",
                    "queryBindId": "7003ba31-2523-4e38-ba56-adf54dbf05cb"
                }
            ]
        },
        {
            "id": "babb673e-43ee-4e9b-8d81-0b7f3d1e41d3",
            "key": "body_weight",
            "name": "Body weight",
            "type": "BETWEEN",
            "values": [
                {
                    "name": "30 - 100",
                    "value": {
                        "min": 30,
                        "max": 100
                    },
                    "queryBindId": "8ac6ad91-4f7a-4709-b701-eed7968ceb12"
                }
            ]
        }
    ],
    [
        {
            "id": "4fc693e6-7865-4075-a84f-66afab0db7a0",
            "key": "blood-group",
            "name": "Blood group",
            "type": "EQUALS",
            "system": "",
            "values": [
                {
                    "name": "B+",
                    "value": "B+",
                    "queryBindId": "250d0bc1-b39d-47b0-98d8-e59a06797fd2"
                }
            ]
        }
    ]
]

Applications can read and write the query store using the getQueryStore and setQueryStore functions.

AST

To allow external systems such as databases and APIs to understand the query, the internal query store is transformed into an AST (Abstract Syntax Tree). The AST is a tree structure with the AstTopLayer type for branches and the AstBottomLayerValue for leaves. Generally the AST structure allows arbitrarily nesting AND and OR operations, although the AST as generated by Lens always has an outer OR operation and an inner AND operation to reflect the struture of the query store. The aforementioned query looks as follows in AST form:

{
    "operand": "OR",
    "children": [
        {
            "operand": "AND",
            "children": [
                {
                    "key": "blood-group",
                    "operand": "OR",
                    "children": [
                        {
                            "key": "blood-group",
                            "type": "EQUALS",
                            "system": "",
                            "value": "A+"
                        }
                    ]
                },
                {
                    "key": "body_weight",
                    "operand": "OR",
                    "children": [
                        {
                            "key": "body_weight",
                            "type": "BETWEEN",
                            "system": "",
                            "value": {
                                "min": 30,
                                "max": 100
                            }
                        }
                    ]
                }
            ]
        },
        {
            "operand": "AND",
            "children": [
                {
                    "key": "blood-group",
                    "operand": "OR",
                    "children": [
                        {
                            "key": "blood-group",
                            "type": "EQUALS",
                            "system": "",
                            "value": "B+"
                        }
                    ]
                }
            ]
        }
    ]
}

Applications can get the AST from the search bar using the getAst function.

Querying a Focus instance

In the samply organization Focus is commonly used to parse the AST and execute the query. Because Focus only communicates over the Beam protocol, Spot is required as an intermediary. Applications can query Focus by listening for the lens-search-triggered event and sending the AST to the backend in the appropriate form:

import { getAst, clearSiteResults, querySpot } from "@samply/lens";

let abortController = new AbortController();
window.addEventListener("lens-search-triggered", () => {
    abortController.abort();
    abortController = new AbortController();
    clearSiteResults();

    const query = btoa(
        JSON.stringify({
            lang: "ast",
            payload: btoa(
                JSON.stringify({ ast: getAst(), id: crypto.randomUUID() }),
            ),
        }),
    );
    querySpot(backendUrl, siteList, query, abortController.signal, (result) => {
        // This is called once per site when its result is received.
    });
});

The querySpot function requires that you set the Spot URL in the Lens options:

"spotUrl": "https://locator-dev.bbmri-eric.eu/backend"

Usually Spot determines the list of sites to query via its SITES environment variable. You can optionally override the list of sites in the Lens options:

"sitesToQuery": ["lodz-test", "uppsala-test", "eric-test", "DNB-Test"]

Learn how to pass results to Lens in the Showing results guide.

Querying Focus with CQL

Some applications send CQL queries to Focus. In this case you need AST to CQL translation code in your application. You can get started by copying and adjusting the ast-to-cql-translator.ts, cqlquery-mappings.ts and measures.ts files from the CCP explorer repository. Sending the query would then look as follows:

import {
    getAst,
    clearSiteResults,
    buildLibrary,
    buildMeasure,
    querySpot,
} from "@samply/lens";

let abortController = new AbortController();
window.addEventListener("lens-search-triggered", () => {
    abortController.abort();
    abortController = new AbortController();

    // AST to CQL translation
    const cql = translateAstToCql(
        getAst(),
        false,
        "DKTK_STRAT_DEF_IN_INITIAL_POPULATION",
        measures,
    );
    const lib = buildLibrary(cql);
    const measure = buildMeasure(
        lib.url,
        measures.map((m) => m.measure),
    );

    clearSiteResults();
    const query = btoa(
        JSON.stringify({
            lang: "cql",
            lib,
            measure,
        }),
    );
    querySpot(backendUrl, siteList, query, abortController.signal, (result) => {
        // This is called once per site when its result is received.
    });
});

Showing results

Lens result format

When your application has queried a backend and receives results from sites, it has to pass these results to Lens so it can display them. Lens expects results of type LensResult, for example:

{
    "stratifiers": {
        "gender": {
            "female": 31,
            "male": 43
        },
        "diagnosis": {
            "C34.0": 26,
            "C34.2": 28,
            "C34.8": 25
        }
    },
    "totals": {
        "patients": 74,
        "samples": 312
    }
}

The totals field contains the total number of patients, samples, etc. The stratifiers field contains stratum counts (e.g. male, female) for each stratier (e.g. gender). The specific stratifiers depend on the application. When you add a chart to your application you specify which stratifier it should display.

Focus can return the Lens result format directly. If you are quering a FHIR server you can convert a FHIR measure report to the Lens result format using the measureReportToLensResult function.

Passing results to Lens

You pass results to Lens using the setSiteResult function. Before you pass a result you may call markSiteClaimed to indicate that the site is available and will deliver results soon. This examples shows how you would pass results from Focus to Lens.

querySpot(
    getBackendUrl(),
    getSiteList(),
    query,
    abortController.signal,
    (result: SpotResult) => {
        const site = result.from.split(".")[1];
        if (result.status === "claimed") {
            markSiteClaimed(site);
        } else if (result.status === "succeeded") {
            const siteResult = JSON.parse(atob(result.body));
            setSiteResult(site, siteResult);
        } else {
            console.error(
                `Site ${site} failed with status ${result.status}:`,
                result.body,
            );
        }
    },
);

Components

Lens provides components to render results.

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;
}

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 Spot and display them in the catalogue. Facet counts are the number of results one would get when only searching for that criteria.

Example of facet counts

To enable facet counts add the following to the Lens options:

"facetCount": {
    "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. You also have to set the Spot URL in the Lens options:

"spotUrl": "https://locator-dev.bbmri-eric.eu/backend"

Lens POSTs an array of sites (e.g. {"sites": ["berlin", "munich"]}) to the appropriate Spot 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. Run git push origin vX.Y.Z to publish the tag that was created by npm run version and start the CI release process.
  5. Verify that CI successfully builds and releases to npm and creates a release on GitHub.

Components

Here we list all the Lens components:

Catalogue

The <lens-catalogue> component renders the catalgue in a collapsible tree structure. The catalogue can be optionally collapsible, and will auto-expand based on configuration. If the options includes a facetCount input, it automatically fetches and updates facet counts on mount.

This component loads all elements from the catalogue store and lensOptions, uses DataTreeElement for each node, and offers customizable toggle behavior. Styling is exposed through ::part() attributes.


Props

PropTypeDefaultDescription
toggle{ collapsable?: boolean; open?: boolean }{ collapsable: true, open: false }Controls whether the catalogue is collapsible and its default open state.

Usage

<lens-catalogue
    toggle={{ collapsable: true, open: true }}
></lens-catalogue>

For a more in depth description of the structure see catalogue guide.


Styling

Part NameDescription
lens-catalogueMain container of the catalogue
lens-catalogue-wrapperWrapper around all top-level nodes
lens-catalogue-toggle-buttonThe expand/collapse toggle button
lens-catalogue-toggle-button-closed-iconIcon when the catalogue is collapsed
lens-catalogue-toggle-button-iconThe icon stlye
lens-catalogue-toggle-button-open-textThe label next to the icon

Chart

The lens-chart component provides a styled wrapper for visualizing data using Chart.js. It handles dynamic display behaviors like toggling chart visibility, showing hints, and rendering a fallback message when no data is available. Some Options are setable via ChartOption and some are set via props.

Example of a lens chart

The component is constructed from a title and a chart. There is a slot after the title and chart for customization. The chart does render either totals or stratifier from the lens-results.


Props

Non of the props are required, however without datakey prop the chart does not known which data to render and will be empty.

NameTypeDefaultDescription
titlestring""Title displayed above the chart.
indexAxisstring"x"Determines the main axis for data display. Use 'y' to flip the chart orientation (horizontal bars instead of vertical)
xAxisTitlestring""Title below the x-axis.
yAxisTitlestring""Title below the y-axis.
clickToAddStateboolean"false"With this prop set to true it will add the clicked item to the search. The rendered element must have the same name as the catalogue item.
headersMap<string, string>""Maps internal result keys to human-readable labels for display. For example, mapping "gender_m" to "Male" or "age_65_plus" to "65+ years".
displayLegendsboolean""With this option a legend of all data items is render below the graph.
chartTypeChartTypeRegistry""Which type of chart is rendered, see types
scaleTypestring"linear"Sets the scale type of the chart. Either "linear" or "logarithmic"
dataKeystring""Looks up the data in the Result store.
perSiteboolean"false"When true, displays aggregated totals per site/location instead of overall totals.
groupRangenumber""lets the user define a range for the labels when only single values are used eg. '60' -> '60 - 69'.
groupingDividerstring""Is the char that combines subgroups into their supergroups like C30, C31.1 and C31.2 into C31
filterRegexstring""Filters data according to the provided regular expression.
groupingLabelstring""Sets divider for grouping stratifier together.
viewScalesboolean"true"Displays the scales on the x-axis and y- axis.
backgroundColorstring[]""Expects an array with hex color strings. These colors will be set in the order which are provided. There is no possibility to map results to certain colors. If no color is set, there are the lens default colors. If you provide more results then colors, they will repeat from the beginning.
backgroundHoverColorstring[]""Similar functionality as the background color, instead the hover colors.

Example

<lens-chart
    title="Gender"
    datakey="gender"
    chartType="pie"
    displayLegends="true"
></lens-chart>

Styling

Part NameDescription
lens-chart-wrapperRoot wrapper with grid layout and background color.
lens-chart-info-button-wrapperPositioned top-right; holds the optional info button.
lens-chart-titleRenders the chart’s title, centered.
lens-chart-overlayCovers the chart area to display messages (e.g. no data).
lens-chart-no-data-availableStyled message shown when there is no chart data.
lens-chart-container-min-width-0Wraps the canvas; forces min-width: 0 for responsiveness.
lens-chart-canvasThe chart rendering surface. Includes width/max-height rules.

Info Button

The lens-info-button component displays a tooltip-style popup that provides users with helpful information or guidance. The popup is toggled by clicking the button.

This component is commonly used across various other components in Lens.

Props

PropTypeDefaultDescription
messagestring or string[]""Text to display inside the tooltip. If an array is passed, each string will be rendered on a new line.
buttonSizestring"16px"Sets both the width and height of the button (e.g., "24px").
alignDialogue"left" | "right" | "center""center"Controls where the tooltip appears relative to the button.
dialogueMaxWidthstring"300px"Maximum width of the tooltip container.
inSearchBarbooleanfalseApplies special styling when used inside a search bar (e.g., white icon with orange hover).

Usage

Here's a basic example of how to use the <lens-info-button> component:

<lens-info-button message="This will inform the user about the app!">
</lens-info-button>

Or with multiple lines:

<lens-info-button
    message='["Line one", "Line two", "Line three"]'
    buttonSize="20px"
    alignDialogue="right"
    dialogueMaxWidth="250px"
>
</lens-info-button>

Styling

Part nameDescription
lens-info-buttonStyles the button container.
lens-info-button-iconDefault icon styling.
lens-info-button-icon-in-search-barStyling override when inSearchBar is true.
lens-info-button-dialogueTooltip wrapper.
lens-info-button-dialogue-messageIndividual message lines.
lens-info-button-dialogue-align-centerTooltip center alignment.
lens-info-button-dialogue-align-leftTooltip left alignment.
lens-info-button-dialogue-align-rightTooltip right alignment.

Exmaple

lens-info-button::part(lens-info-button-icon) {
    color: var(--primary-color);
}

Search Modified Display

<lens-search-modified-display> displays a visual cue when the current query has been modified. It listens to the QueryStore and only renders its slotted content if a change has occurred.


Usage

Use the default <slot> to pass in content:

<lens-search-modified-display>
    <b>You have unsaved changes. Please click "Search" to update results.</b>
</lens-search-modified-display>

Styling

Part NameDescription
lens-query-modified-display-wrapperWraps the content and applies border and spacing

Example

lens-search-modified-display::part(lens-query-modified-display-wrapper) {
    background-color: var(--light-orange);
    color: var(--dark-orange);
    font-weight: bold;
}

Negotiate Button

The lens-negotiate-button component allows users to initiate a data request. It becomes active when one or more (site/data sources) are selected. Depending on the configuration, it either dispatches a general event or starts a negotiation through the BBMRI-ERIC Negotiator service. The button is disabled by default when no data source is selected. Fires a global lens-negotiate-triggered event when clicked.

Optionally integrates with the BBMRI-ERIC Negotiator when type is set to "Negotiator".

Props

PropTypeDefaultDescription
titlestring"Request Data"The label shown on the button.
typestring""Determines the negotiation behavior. Use "Negotiator" to enable integration with BBMRI-ERIC.

Behavior

  • The button is disabled when no data sources are selected (datarequestsStore is empty).

  • Clicking the button always triggers a global browser event:

    window.dispatchEvent(new CustomEvent("lens-negotiate-triggered"));
    
  • If type is "Negotiator", the internal bbmriNegotiate() function is also called, passing the current data request list.

Usage

<lens-negotiate-button type="Negotiator"></lens-negotiate-button>

Or with a custom label:

<lens-negotiate-button
    type="Custom"
    title="Start Request"
></lens-negotiate-button>

Styling

Part nameDescription
lens-negotiate-buttonBase button styling.
lens-negotiate-button-activeApplied when the button is enabled and clickable.
lens-negotiate-button-disabledApplied when the button is inactive (no data selected).
lens-negotiate-button-titleStyles the text inside the button.

Example

lens-negotiate-button::part(lens-negotiate-button-active) {
    background-color: var(--custom-green);
}

Query Explain Button

The lens-query-explain-button is a wrapper around the lens-info-button that displays a human-readable version of the current query. When the QueryStore is empty, the noQueryMessage message is shown. If just a queryItem is passed as a prop it will just display this part.

Props

PropTypeDefaultDescription
queryItemQueryItem | undefinedundefinedIf provided, a human-readable explanation of this query item is displayed. If not, the current global query from queryStore is used.
noQueryMessagestring"Search for all results"Message shown when no query is present.
inSearchBarbooleanfalseApplies compact and minimal styling for use inside a search bar.

Usage

<lens-query-explain-button></lens-query-explain-button>

Styling

Part nameDescription
lens-query-explain-buttonWrapper around the InfoButtonComponent when not in search bar mode. Useful for applying borders, padding, etc.

Example

lens-query-explain-button::part(lens-query-explain-button) {
    border-color: var(--primary-color);
    padding: 8px;
}

Query Spinner

The <lens-query-spinner> component provides a visual indication that a query is currently being processed. It automatically displays a spinning loader when a search is triggered and hides it once responses have been received from all data sources. The spinner listens on lens-search-triggered event and waits till all LensResults are not claimed anymore.


Usage

The spinner appears when a search is in progress and disappears once all site responses are received.

<lens-query-spinner size="24px" />

Props

PropTypeDefaultDescription
sizestring20pxSets the height and width of the spinner

Result Summary

The lens-result-summary component displays a compact summary of result metrics defined in the Lens options. It is typically used to show overall values, such as the total number of patients found across all data sources or how many sources responded successfully.


Example

To use it, define the configuration in the Lens options and include the component in your HTML:

"resultSummaryOptions": {
    "title": "Results",
    "infoButtonText": "This is a tooltip",
    "dataTypes": [
        {
            "title": "Patients",
            "dataKey": "patients"
        }
    ]
}


Usage

The component doesn't use any props and is only set via the options

<lens-result-summary></lens-result-summary>

CSS Parts

This component uses ::part() selectors to expose internal styles for customization.

Part NameDescription
lens-result-summaryGrid wrapper for the entire result summary layout.
lens-result-summary-headerContainer for the title and optional info button.
lens-result-summary-headingAlignment wrapper for the heading text.
lens-result-summary-header-titleFlex layout for the header text and info button.
lens-result-summary-contentFlex container for the individual population summaries.
lens-result-summary-content-typeHolds each population title and its respective count.

Styling Example

[part~="lens-result-summary-header"] {
    background: #126154;
}

Result Table

The lens-result-table component displays a paginated and sortable table of totals from the LensResult. It uses the configured Lens options and listens to the ResultStore.

The table is setup with some props and some via the options. For exmaple the Columns are defined in the table options.

The table is automatically populated with data sources that are marked as claimed in the ResultStore. You can provide user-friendly labels for internal site keys via the siteMappings option.

If a data source has not yet been fully claimed, the table shows a "loading" message. This message is customizable via the translation key loading, see translation.

Each row includes a checkbox to select that data source for a data request. The selection is tracked in the DataRequestStore.


Props

PropTypeDefaultDescription
titlestring""Optional title displayed above the table.
pageSizenumber10Number of rows per page.
pageSizeSwitcherbooleanfalseIf true, enables a dropdown to change the number of rows shown.

Slots

Slot NameDescription
lens-result-above-paginationRenders content above the pagination controls.
beneath-paginationRenders content below the pagination controls.

Example

To use the table, define the tableOptions and include the component in your HTML:

"tableOptions": {
    "headerData": [
        {
            "title": "Sites",
            "dataKey": "site"
        },
        {
            "title": "Patients",
            "dataKey": "patients"
        }
    ]
}
<lens-result-table title="Result Table" pageSize={25} pageSizeSwitcher={true} />

Styling

Part NameDescription
lens-result-table-titleThe title heading above the table.
lens-result-tableThe table element itself.
lens-result-table-headerThe <thead> element.
lens-result-table-header-rowThe header row <tr>.
lens-result-table-header-cellGeneric header cells.
lens-result-table-header-cell-checkboxHeader cell containing the "select all" checkbox.
lens-result-table-header-datatypeHeader cells for data type columns (sortable, may include info buttons).
lens-result-table-header-checkboxThe checkbox used to select all rows.
lens-result-table-table-bodyThe <tbody> containing the data rows.
lens-result-table-paginationContainer for pagination controls.
lens-result-table-pagination-buttonPrevious and next arrow buttons.
lens-result-pagination-pagination-previousSpecifically styles the "previous" button.
lens-result-pagination-pagination-nextSpecifically styles the "next" button.
lens-result-table-pagination-pagenumberDisplays the current page number and total page count.
lens-result-table-pagination-switcherContainer for the page size switcher dropdown.

Search Bar

The lens-searchbar component offers an interface for exploring of all single-select items. It serves as the primary interface for users to search, apply, and adjust query criteria. Selected items appear as interactive chips within the component, giving users a clear visual of their active filters. Users can easily refine their search by removing individual values or entire criteria directly from the chip display.


Behavior & Functionality

  • Search Input with Autocomplete:

    • Opens dropdown after 3+ characters.
    • Filters and displays grouped suggestions with optional counts and descriptions.
    • Keyboard navigation support (focusedItemIndex).
  • Search Chips:

    • Displays selected criteria as removable chips.
    • Nested value-level and item-level delete buttons.
    • Each chip displays:
      • Criterion name
      • Values
      • Optional explain button via QueryExplainButtonComponent
  • Autocomplete Items:

    • Rendered with bold-highlighted matches.
    • Supports facet counts from $facetCounts.
    • Fully keyboard- and mouse-navigable.
  • Clear Group Button:

    • Clears the entire group when clicked.
    • Emits a clear-search event.

Props

PropTypeDefaultDescription
noMatchesFoundMessagestring"No matches found"Message shown when no autocomplete options are found.
typeMoreMessagestring"Search will start with 3 inserted letters"Message shown when input is too short for autocomplete.
placeholderTextstring"Type to filter conditions"Placeholder in the search input field.
indexnumber0Used to manage multiple search bars (e.g., in groups or filters).

Events

Triggered when the group clear button is clicked.

<lens-search-bar on:clear-search={handleClear} />

Example

<lens-search-bar
    placeholderText="Search filters..."
    noMatchesFoundMessage="No criteria match"
    typeMoreMessage="Type at least 3 characters to search"
    index="{1}"
/>

Styling

PartPurpose
lens-searchbarWrapper for the entire component
lens-searchbar-inputMain input element
lens-searchbar-chip, chip-name, chip-itemVisual query chips
lens-searchbar-autocomplete-optionsAutocomplete container
lens-searchbar-autocomplete-options-itemIndividual result
lens-searchbar-autocomplete-options-item-focusedHighlighted result
lens-searchbar-autocomplete-options-item-descriptionOptional description
lens-searchbar-autocomplete-options-item-facet-countFacet count badge

Seach Bar Multiple

The lens-search-bar-multiple is a wrapper for multitple searchbars.

Each search bar is visually separated by an "or" indicator, and users can append additional bars using a dedicated add button. It is primarily used for OR-based query logic where each search bar represents an independent branch of the search criteria.

A default <slot /> is provided to insert additional UI elements, such as a search or submit button.


Props

PropTypeDefaultDescription
noMatchesFoundMessagestring"No matches found"Message shown when no autocomplete options are available.
placeholderTextstring"Type to filter conditions"Placeholder text used across all search bars.

Example

You may also include a search button using the default slot:

<lens-search-bar-multiple>
    <lens-search-button title="Apply Filter"></lens-search-button>
</lens-search-bar-multiple>

Styling

The component exposes styling hooks via part attributes for full control over layout and appearance.

PartPurpose
lens-searchbar-multipleWrapper around the full multi-search component
lens-searchbar-multiple-wrapperWrapper for each search bar row
lens-searchbar-multiple-add-buttonAdd button to insert a new search bar group
lens-searchbar-multiple-or-indicatorVisual "or" indicator between search bars

Search Button

The lens-search-button component triggers a search based on the current query. It is a visually styled button that emits an event when clicked. The button can be disabled via a prop, or will be disabled when a error state is entered in the searchbar.

Features

  • Emits a event lens-search-triggered when clicked. With this event you would trigger some sort of data request for data provider.

Props

PropTypeDefaultDescription
titlestring"search" (localized)The button's visible label. Uses a translation helper by default.
disabledbooleanfalseWhether the button is disabled and non-interactive.

Usage

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

Styling

Part nameDescription
lens-search-buttonMain button container.
lens-search-button-magnifying-glassIcon shown at the start of the button (rotated magnifying glass).
lens-search-button-titleText label inside the button.

Example

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

lens-search-button::part(lens-search-button-magnifying-glass) {
    font-size: 1.5rem;
    color: var(--highlight);
}

Release notes

Version 0.6.0

Migration guide

New <lens-query-explain-button> component

In earlier versions of Lens, we used a specialized version of the <lens-info-button> to display the current query or a portion of it. This functionality has now been refactored into the dedicated <lens-query-explain-button> component. Update your app as follows:

-<lens-info-button noQueryMessage="Empty Query" showQuery={true}></lens-info-button>
+<lens-query-explain-button noQueryMessage="Empty Query"></lens-query-explain-button>

Querying the backend

In the new version, Lens will no longer query backends itself when the search button is clicked. Instead, the application must read the current query from the search bar, send the query to a backend to get results, and then pass the results to Lens so it can show them.

To give a short summary of the new paradigm, the application listens to the global lens-search-triggered event, then retrieves the current query using the new getAst function, optionally uses the new querySpot function to send the query to a backend, and then passes the results to Lens via the new markSiteClaimed and setSiteResult functions. More details and example code can be found in the Querying a backend and Showing results guides.

The setSiteResult function accepts the new LensResult format that is independent of the FHIR standard. The latest version of Focus can return the new result format directly. If you have a FHIR result you can convert to the new format using the measureReportToLensResult function.

A number of related APIs have been removed in favor of the new paradigm:

  • The backend field has been removed from the Lens options
  • The emit-lens-query event has been renamed to lens-search-triggered and does no longer contain the details field
  • The lens-responses-updated event has been removed

Catalogue prop changes

If you are using the collapsible catalogue feature, the texts for the expand/collapse button are now no longer specified as props:

 <lens-catalogue
-    texts={{
-        collapseButtonTitle: 'My custom expand text',
-        expandButtonTitle: 'My custom collapse text',
-    }}
     toggle={{ collapsable: true }}
 ></lens-catalogue>

If you specified any other texts in the texts prop you can remove them as well as they are no longer used. The expand/collapse button texts are now specified in the Lens options as translations:

"texts": {
    "catalogue_expand": {
        "en": "My custom expand text"
    },
    "catalogue_collapse": {
        "en": "My custom collapse text"
    },
}

The treeData prop previously used to provide the catalogue to Lens has been removed. Use the setCatalogue function instead.

CSS class name prefixes updated

Starting with version 0.6.0, all Lens CSS class names now begin with the prefix lens- followed by the component name. This change improves readability, provides clearer structure, and helps avoid unintentional conflicts with existing styles or overrides.

If you are overriding any Lens component styles, please review your class names to ensure they match the new naming convention.

Query in URL

The URL now always updates to stay in sync with the current query so can share and bookmark queries via the URL. You can disable this in the Lens options:

"autoUpdateQueryInUrl": false

Removed iconOptions

Because this feature was rarely used icons in Lens can no longer be customized. Remove these lines from your Lens options:

"iconOptions": {
  "deleteUrl": "delete_icon.svg",
  "infoUrl": "info-circle-svgrepo-com.svg",
  "toggleIconUrl": "right-arrow-svgrepo-com.svg",
  "addIconUrl": "long-right-arrow-svgrepo-com.svg"
},

You can also remove the icon image files if your app does not use them anywhere else.

Removed <lens-data-passer>

The <lens-data-passer> component which was previously used to access a number of APIs has been removed. Most of these APIs have now been exported as regular TypeScript functions and some have been deprecated:

Old APIReplacement
getQueryAPIgetQueryStore
getResponseAPIApps are now responsible for querying the backend so they have access to the responses without need for an API.
getAstAPIgetAst
updateResponseStoreAPIsetSiteResult, markSiteClaimed
getCriteriaAPIThis API was previously used to resolve subgroups in the AST (e.g. replace C50.% with C50.1, C50.2, etc.). From Lens version 0.6.0 onwards the AST returned by getAst() already has subgroups resolved for you.
getCatalogueAPIApps control the catalogue so there should be no need to read it from Lens.
setQueryStoreAPIsetQueryStore
setQueryStoreFromAstAPIOnly OVIS uses this API and we have provided a replacement for them but use of this function in new code is discouraged.
addStratifierToQueryAPIaddItemToActiveQueryGroup
removeItemFromQueryAPI, removeValueFromQueryAPIIf possible, use getQueryStore and setQueryStore instead.

Removed <lens-options> component

The <lens-options> component has been removed. Instead use the setOptions and setCatalogue functions. When removing the <lens-options> tag you should also be able to remove the {#await} block that was needed previously. For example:

 <script>
+    import {
+        setOptions,
+        setCatalogue,
+        type LensOptions,
+        type Catalogue,
+    } from "@samply/lens";
+    import options from "./config/options.json";
+    import catalogue from "./config/catalogue.json";
+    onMount(() => {
+        setOptions(options as LensOptions);
+        setCatalogue(catalogue as Catalogue);
+    });
-    const jsonPromises = fetchData(optionsUrl, catalogueUrl);
 </script>

-{#await jsonPromises}
-    <!-- render a loading spinner -->
-{:then { optionsJSON, catalogueJSON }}
-    <lens-options {catalogueJSON} {optionsJSON} {measures}></lens-options>
-{:catch someError}
-    <!-- render the error -->
-{/await}

Renamed types

We've renamed some types to indicate that they are specific to the FHIR standard:

Old nameNew name
SiteDataFhirMeasureReport
MeasureFhirMeasure
MeasureItemFhirMeasureItem

Facet counts use spotUrl

Facet counts now use the spotUrl from the Lens options with /prism automatically appended:

+"spotUrl": "https://locator-dev.bbmri-eric.eu/backend",
 "facetCount": {
-    "backendUrl": "https://locator-dev.bbmri-eric.eu/backend/prism",
     "hoverText": {
         "gender": "Matching patients for this criterion only",
         ...
     }
 },

Removal of catalogueKeyToResponseKeyMap option

In previous versions catalogueKeyToResponseKeyMap was used in the <lens-chart> component to map catalogue keys to response keys with different names. The map has been removed and the <lens-chart> component now takes the data key directly. Remove the map in the Lens options:

-"catalogueKeyToResponseKeyMap": [
-    [
-        "age_at_diagnosis",
-        "donor_age"
-    ],
-]

And update your usage of <lens-chart> accordingly:

-<lens-chart catalogueGroupCode="age_at_diagnosis">
+<lens-chart dataKey="donor_age">

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.