Reading time: 19 minutes and 45 seconds.

What's new?

This page describes some of the latest additions to the mods API and SDK.

No matter which mods API version you want to target, you should always use the latest compatible version of the NPM packages:

If you have started from an SDK template, simply run npm update to get the latest compatible versions.

Mods API 2.4

This version of the mods API extends the multiple data view definition feature introduced in version 2.3 by allowing visualization mods to define a dynamic number of data views. This feature enables the ability to develop mod visualizations with a dynamic number of layers of a certain type. Visualization mods also contain many more additions such as the ability to extend the axes of another data view, new transaction APIs, and new readable APIs.

Version 2.4 does not contain any addition to the action mods framework itself, but many API additions have been made as part of Spotfire 14.8.

Visualization mods

Dynamic layers

With version 2.4 of the visualization mods API you can define layer types via the "layers" property in the mod manifest. Layer types may contain a single main data view definition and many named data view definitions. A layer may also define its own mod properties whose values are unique to each instance of the layer. A layer can be added via Spotfire’s UI (unless "disableAddRemove" is set to true) or via the ModLayers.add API. You can think of layers as sub visualizations inside your visualization mod, and the layer instance type ModLayer supports many of the same APIs that the mod visualization does.

The dynamic nature of layers allow you to create advanced visualizations which contain sub elements of a certain type. For instance you can now create a graphical table where each column defines a mini visualization such as a scatter plot or a bar chart. Another use-case is layered visualizations such as map charts where you may have many feature or scatter layers stacked on top of each other.

To keep axes in sync across data views you can inherit the axis and data configuration via the "extendedAxes" property. This property takes an array of axis names defined further up in the manifest.

To see a simple example of a visualization using the layers feature checkout the ts-dev-graphical-table example project. This project implements a graphical table supporting the bar chart and scatter plot mini visualizations. It uses the extended axes feature to sync the “Row by” axis across mini visualizations.

The most straightforward way to show layers in a visualization mod is to add the ModVisualization.layers readable to your main render loop and render each layer instance consecutively. For example:

Spotfire.initialize(async (mod: Spotfire.Mod) => {
    const reader = mod.createReader(mod.visualization.layers());
    const context = mod.getRenderContext();

    reader.subscribe(render);

    async function render(layers: Spotfire.ModLayers) {
        // ...
        for (const layer of layers.items) {
            const data = await layer.data();
            const errors = await data.getErrors();
            if (errors.length > 0) {
                // Show the error.
                continue;
            }

            // Render the layer according to its type.
            switch (layer.type) {
                case "FeatureLayer": {
                    // Render feature layer
                }
                // ...
            }
        }
    }
});

The data views for layers are eagerly materialized for best performance in most cases. If your mod visualization requires a more advanced rendering flow you can pass a LayerMode argument to the layers readable to gain greater control over when data is materialized. For instance passing "LazyData" means Spotfire will not attempt to materialize the data view until the ModLayer.data readable is awaited. For total control you can pass the "CollectionOnly" layer mode and setup individual readables on each layer instance.

Extended axes

Mod visualization containing multiple data views often need to share some common splitting axes across all views being visualized. For instance a graphical table contains a top-level “Row by” axis which every mini visualization needs to respect. Previously this required duplicating the “Row by” axis across all data view definitions and adding error modes when the axes’ data tables and expression went out-of-sync. The "extendedAxes" property allows you to solve this issue by specifying a list of axes you want to inherit from an ancestor data view. For instance the manifest of a graphical table containing a bar chart mini visualization may look something like this:

{
    "apiVersion": "2.4",
    "dataViewDefinition": {
        "axes": [{ "name": "Row by", "mode": "categorical", "placement": "left" }]
    },
    "layers": [
        {
            "type": "Bar Chart",
            "dataViewDefinition": {
                "extendedAxes": ["Row by"],
                "axes": [
                    {
                        "name": "Category",
                        "mode": "categorical"
                    },
                    {
                        "name": "Value",
                        "mode": "continuous"
                    }
                ]
            }
        }
    ]
}

When configuring a bar chart instance in this visualization the “Data” configuration will not be availble since it is controlled by the top-level data view containing the “Row by” axis. The axis can be read via the layers regular data readable similar to any other axis, but attempting to configure it will throw an error since its expression is controlled by the top-level data view.

Title

There is a new readable ModVisualization.title that allows mod to read, subscribe to and modify the mod title. Dynamic layers also has an identical readable ModLayer.title.

Pause/resume

Sometimes a mod has a need to pause rendering, for example when the user begins a long running operation like area marking. In version 2.4 the reader can be temporarily stopped using the pause function and resumed using the resume function. While paused, no document changes will cause the render callback function to be invoked. Upon resume, the API will first await any current modifications, like marking operations, before the next render callback will occur. This will ensure a non flickering experience on resume.

Readable API additions

All readables in version 2.4 and upwards will cache its result internally, and return new instances only when there is a new value available on the backend. The underlying mechanism used is the same as when a reader invoke its registered callback function. This is an optimization required for the new layer functionality, and should in most cases not require any code changes in existing mods. Only mods that rely on reference equality on readables will need to be refactored. A performance benefit is that awaiting a readable like (await mod.property("myProperty")).value() will immediately return the cached result until the mod property changes in the backend.

In version 2.4 it is also valid to create multiple readers for a single readable. For example in the example below, every "layerReader" could include a mod global property mod.property("renderStyle") that would affect the rendering of all layers.

Dynamically creating readers

Before the introduction of dynamic layers, all readables that could be used when creating a reader was statically known from the API and the manifest. With dynamic layers, it is not always possible to statically know what readables that a reader require. For example, a mod that renders layers independently from eachother may want to set up a separate reader for every layer instance. While bookkeeping of new and removed layers needs to be performed by the mod itself, it may be handy to create one reader per layer. This is per se not

Spotfire.initialize(async (mod: Spotfire.Mod) => {
    const reader = mod.createReader(mod.visualization.layers("CollectionOnly"));
    const layerReaders = new Map<String, Spotfire.ReaderSubscription>();
    reader.subscribe(render);

    async function render(layers: Spotfire.ModLayers) {
        const layerIds = layers.items.map((l) => l.id);
        const newLayers = layers.items.filter((l) => !layerReaders.has(l.id));
        const removedLayers = Array.from(layerReaders.keys()).filter((k) => !layerIds.includes(k));

        for (const layer of newLayers) {
            layerReaders.set(layer.id, createReader(layer.id, layer));
        }

        for (const id of removedLayers) {
            layerReaders.get(id)?.cancel();
            layerReaders.delete(id);
            console.log(`Removed layer: ${id}`);
        }
    }

    function createReader(id: string, layer: Spotfire.ModLayer) {
        console.log(`Add layer: ${id}`);
        const layerReader = mod.createReader(layer.data(), mod.property<number>("renderStyle"));
        return layerReader.subscribe(renderLayer);

        function renderLayer(data: Spotfire.DataView, renderStyle: Spotfire.ModPropertyValue<number>) {
            console.log(`Render layer: ${id} with style ${renderStyle.value()}`);
        }
    }
});
Using Reader.hasReadableChanged

A layered mod that requires all layers when rendering may still want to query the API for udpate status on layer readables, similar to the Reader.hasValueChanged method. This method will not work for layer.data() in the example below since it expects the resolved value, not the readable itself. There is a new similar method in API version 2.4, Reader.hasReadableChanged, that will tell whether a readable has a new value available since the last query. This is subtly different from its sibling method that tells whether the current argument has a new value compared to the last invocation.

Spotfire.initialize(async (mod: Spotfire.Mod) => {
    const reader = mod.createReader(mod.visualization.layers());
    reader.subscribe(render);

    async function render(layers: Spotfire.ModLayers) {
        for (const layer of layers.items) {
            if (reader.hasReadableChanged(layer.data()))
            {
                // Render the layer.
            }
        }}
});

Any modification requiring prior user activation

Mods specifying API version 2.4 will require a prior user gesture before attempting to modify any part of the document. The main purpose for this is to prevent a mod from modifying the document in a render loop, which is an API misuse that mainly breaks the undo/redo mechanism, export errors or other more subtle bugs. Any user gesture, such as key down, mouse down, or scroll wheel, is enough to allow API modifications. Mods specifying API version less than 2.4 will in Spotfire 14.8 or later yield a browser console warning.

Invisible and sticky transactions

When modifying the Spotfire analysis via the visualization mods API, it may sometimes be useful to control the undo/redo stack, which is done using the transaction API. Prior to 2.4, there was only possible to group a set of modifications into one undo step, and for most usages this was made implicitly by the API.

  • Sticky transaction will automatically merge a set of modifications with the preceding transactions if they share the supplied transaction identifier. This type of transaction is useful for example when the mod updates a mod property when dragging a slider handle, resulting in only the final value being recorded in the undo/redo stack.

  • Invisible transaction allows the mod author to make document modifications that will be invisible in regards to the undo/redo stack. For example if the mod as a notion of currently active section for interaction within the mod, this may be recorded as an invisible transaction. These changes will never produce its own undo/redo step.

Reading data from dual mode axis

Reading row data for dual mod axis is in API versions prior to 2.4 somewhat cumbersome. Before using any of the methods DataViewRow.categorical or DataViewRow.continuous, you first need to check th current axis mode to know which of the two methods to use, or the method will throw an error.

With API version 2.4 there are two new sibling methods; DataViewRow.tryCategorical or DataViewRow.tryContinuous. These methods will return null for dual mode axis that currently has an empty expression or does not match the current mode. They will however still throw an error if the "axisName" does not match a dual mode axis.

Action mods

No new additions to the action mods framework have been added. However, Spotfire 14.8 introduces new features which are supported in the API. These include:

Mods API 2.3

Version 2.3 of the mods API introduces the ability to define multiple data views in visualization mods. This feature enables the ability to develop mod visualizations which contain sub-visualizations (layers), or which consume data from multiple data tables.

  1. Update the "apiVersion" version in the mod manifest to "2.3".
  2. Update the version of "@spotfire/mods-api" in the package.json file to ~2.3.0.
  3. Run npm update.

Visualization mods

Version 2.3 introduces the biggest new feature to visualization mods since its release; the ability to define multiple data view definitions. Previously, a mod could only contain a single data view definition, declared in the mod manifest using the "dataViewDefinition" property. This limited mods to only being able to consume data from a single data table, or to require the user of a mod to prepare their data in such a way that it could fit the data view definition. Now we have added the ability to define multiple data view definitions via the "namedDataViewDefinitions" property in the manifest.

For example, a network graph may want to consume data from two different data tables, one table which contains the node definitions, and another which describes the edges between nodes. For such a mod, these definitions can be specified as follows:

{
    "apiVersion": "2.3",
    "dataViewDefinition": {},
    "namedDataViewDefinitions": [
        {
            "name": "Nodes",
            "icon": "nodes.svg",
            "axes": [
                {
                    "name": "NodeID",
                    "mode": "categorical",
                    "dropTarget": { "icon": "Column", "description": "Set {0} on the NodeID axis" }
                }
            ]
        },
        {
            "name": "Edges",
            "icon": "edges.svg",
            "axes": [
                {
                    "name": "From",
                    "mode": "categorical",
                    "dropTarget": { "icon": "Column", "description": "Set {0} on the Edge From axis" }
                },
                {
                    "name": "To",
                    "mode": "categorical",
                    "dropTarget": { "icon": "Column", "description": "Set {0} on the Edge To axis" }
                }
            ]
        }
    ]
}

Note that we have left the "dataViewDefinition" completely empty to illustrate the fact that you do not need to declare a “main” data view definition. Data view definitions defined within the "namedDataViewDefinitions" must declare a name and may optionally declare an icon (shown in the contextual visualization properties panel). For the manifest defined above, the data can be consumed using the familiar mod.visualization.data API by providing the name of the data view you want to subscribe to. Note that other APIs used to get or set data table or axis information now also take an optional name parameter for the cases where they are defined in a named data view definition. The render function for the network graph can look something like this:

Spotfire.initialize(async (mod: Spotfire.Mod) => {
    const reader = mod.createReader(mod.visualization.data("Nodes"), mod.visualization.data("Edges"));
    const context = mod.getRenderContext();

    reader.subscribe(render);

    async function render(nodesDataView: Spotfire.DataView, edgesDataView: Spotfire.DataView) {
        const errors = (await Promise.all([nodesDataView.getErrors(), edgesDataView.getErrors()])).flatMap(
            errs => errs
        );
        if (errors.length > 0) {
            mod.controls.errorOverlay.show(errors);
            return;
        }

        const [nodeRows, edgeRows] = await Promise.all([nodesDataView.allRows(), edgesDataView.allRows()]);
        if (nodeRows == null || edgeRows == null) {
            mod.controls.errorOverlay.show("No nodes or edges to render.");
            return;
        }

        mod.controls.errorOverlay.hide();

        // Render the network graph.

        context.signalRenderComplete();
    }
});

The nodeRows and edgeRows results may contain a different number of rows in the case where they are configured with different data tables. A limitation is that axes cannot be shared between data view definitions. This means that it is up to you as the mod developer to perform any necessary matching, and to keep axes consistent between data view definitions. For instance, to support trellising, you need to add a “Trellis by” axis to both “Nodes” and “Edges”, and report errors if the axes are out of sync or become inconsistent.

Action mods

No new additions to the action mods framework have been added. However, Spotfire 14.7 introduces many new features which are supported in the API. These include:

Mods API 2.2

Version 2.2 of the mods API introduces optional axes to visualization mods, as well as exposing new action mods APIs for working with reference layers. Using API features introduced in the 2.2 API requires Spotfire version 14.6 or later. To upgrade an existing mod, follow these steps:

  1. Update the "apiVersion" version in the mod manifest to "2.2".
  2. Update the version of "@spotfire/mods-api" in the package.json file to ~2.2.0.
  3. Run npm update.

Action mods

No new additions to the action mods framework has been added however, Spotfire 14.6 introduces many new features which are supported in the API. One such new feature is “reference elements”, which allows you to add reference layers to certain visualizations and show trend lines. As an example on how to use the new APIs for this feature, see the snippet “Add a reference layer showing trend lines”.

Visualization mods

Version 2.2 of the visualization mods API adds the ability to mark an axis “optional”. This can be done by setting the "optional" property of the axis object in the manifest to true. An axis marked optional indicates that it should not be displayed in the new visualization properties panel when in an unconfigured state. It also makes it possible to clear the axis expression by removing the card from the visualization properties panel if it has been configured.

To control the automatic configuration of an optional axis, the API also introduces the "disableOnAutoConfigure" property on the "automaticConfiguration" object. An optional axis with a defined automatic configuration will be configured when the mod visualization is initially added to the document. By setting "disableOnAutoConfigure" to false, the axis will only be automatically configured when its axis card is added in the visualization properties panel.

Mods API 2.1

Version 2.1 of the mods API contains many improvements to both action mods and visualization mods. Using API features introduced in the 2.1 API requires Spotfire version 14.5 or later. To upgrade your mod, follow these steps:

  1. Update the "apiVersion" field in the manifest to "2.1".
  2. Update the version of @spotfire/mods-api in the package.json file to ~2.1.0.
  3. Run npm update.

Action mods

Version 2.1 of the API contains a lot of improvements and new features for action mods.

New parameter types

This API introduces two new types with two variants each: DataColumn (array: true/false) and DataViewDefinition (singleColumn: true/false). Additionally, it is now possible to define parameters as optional (see Optional), to limit numerics to a range (see Numeric ranges), and to specify a set of allowed values for string parameters (see Enum).

DataColumn

Scripts can now define DataColumn as a possible parameter type. This can be done by setting "type": "DataColumn" for the parameter in the manifest. Defining a parameter of this type will allow you to select a column from an existing data table during configuration or when the parameter is prompted.

Data column parameters are useful in scripts which previously required a GUID or column name as a parameter. Previously you had to pass a GUID or string to DataColumnCollection.TryGetValue and check that the method returned true, likely throwing an error if not. This was clunky, required writing boilerplate code, and prone to input errors such as misspelling the column name or entering an incorrect GUID.

Take for instance the “Read the values of a column” snippet which previously started with this code:

export function readColumnValues({ document, table, column }: ReadColumnValuesParameters) {
    const dataColumn = OutParam.create(DataColumn);
    if (!table.Columns.TryGetValue(column, dataColumn.out)) {
        throw new Error(`Data table '${table.Name}' contains no column with name '${column}'.`);
    }

    // ...
}

By changing the column parameter type from "String" to "DataColumn", this code can be completely removed. The table parameter is no longer necessary because it can be retrieved via the column’s document node context. See the snippet for the updated code and manifest.

The new DataColumn type also supports setting property "array": true to allow multiple columns as a value. When configured as an array, the script argument will have type Iterable<DataColumn> and can be consumed by the script via for ... of loops or functions which operate on iterables such as Array.from.

Additionally, the set of possible columns that the user is allowed to choose from can be limited by specifying allowedDataTypes. For example, to limit to only integer, columns set "allowedDataTypes": ["Integer"].

DataViewDefinition

Scripts can now define DataViewDefinition as a possible parameter type. This can be done by setting "type": "DataViewDefinition" for the parameter in the manifest. Defining a parameter of this type can be configured by selecting columns, a column search expression, or a custom expression. The data view definition can also contain a number of limitations, such as limit by marking or filtering scheme (unless "disableLimitations": true is set in the manifest).

A data view definition is a comma separated list of column expressions. The data view can be limited to only a single column expression by setting the "singleColumn": true property.

The main purpose of this parameter is to be passed along to data functions. This is made possible by a new overload of DataFunctionInputCollection.SetInput which takes a ActionDataViewDefinition as its second argument, as an example see snippet “Add a data function using a data view definition”.

Optional

A parameter can be marked as optional by defining "optional": true. When a parameter has been defined as optional, it is not necessary to provide an input value during configuration. Adding optional parameters to existing scripts does not invalidate existing configurations. When defining a parameter of some type T as optional the argument will be typed as T?, meaning that the value may either be provided or undefined.

Optional parameters are a great way to update existing scripts without breaking existing usages (the API will remain stable). They can also be used to reduce the amount of configuration in cases where you can assume some default value for an optional parameter.

Enum

You can now limit string parameters to allow specific values by specifying "enum": ["Value 1", "Value 2"]. A string parameter limited in this way will provide the user with a drop-down list of the available options during configuration. Adding a value to an existing enum will not invalidate existing configurations and will therefore not break existing usages.

Numeric ranges

Numeric parameters can now be restricted to a specified range in the manifest. For instance, for a Real that is used to represent a value between 0% and 100%, you can restrict the parameter to that range by defining "range": { "min": 0, "max": 1 }. Another useful range would be an Integer used to represent a count starting from 0. This can be achieved by specifying "range": { "min": 0 }. For more information, see the parameter documentation for Numerics.

New APIs

Some new and previously existing .NET APIs have now been made available to action mods. These include:

Add resources to your action mod

File resources, specified using the files property in the manifest, can now be accessed as Streams inside a script. Specifying the path relative to manifest in the files field will include the file in the mod, the mod can then be accessed via the resources parameter provided to every script’s entry point function.

Let’s try to add a PNG image to a text area on the active page.

  1. Create a static folder in your mod project next to the manifest.

  2. Add a PNG image in the folder called my-image.png.

  3. In the manifest, reference your image by adding "files": ["static/my-image.png"].

    All scripts in the mod should now be provided with a resources parameter. If you have the SDK build watcher running you can see this in the env.d.ts file.

  4. Create a new script via npx @spotfire/mods-sdk add-script add-image-to-text-area and open the script file.

If you are using the latest SDK, the entrypoint should automatically destructure the resources field from the parameter object. For existing scripts, simply add resources after document.

Our image can now be retrieved via resources.LoadResource("static/my-image.png"). This method returns a Stream, which is a type accepted by many APIs. Let’s modify the script so that it creates a text area, adds the image to the text area’s image collection, and displays it as HTML. The script should look like this:

const { HtmlTextArea } = Spotfire.Dxp.Application.Visuals;

export function addImageToTextArea({ document, application, resources }: AddImageToTextAreaParameters) {
    const page = document.ActivePageReference;
    if (!page) {
        throw new Error(`Cannot create a text area without an active page`);
    }

    const textArea = page.Visuals.AddNew(HtmlTextArea);
    textArea.Images.Add("my-image", resources.LoadResource("static/my-image.png"));
    textArea.HtmlContent = `<img src="my-image" />`;
    textArea.Title = "Text are containing an image";
}

RegisterEntryPoint(addImageToTextArea);

Spotfire previously used to re-fetch the entire mod on any file update, which would cause performance to slow down when mods with large resource files were developed. Make sure you are using version 1.2 or later of mods-dev-server to allow Spotfire to only fetch the files that have actually changed.

Unique icon per script

Each script can now specify its own unique SVG icon, which will show up in various parts of the UI such as floating buttons, title bar buttons, and in the Actions flyout. Scripts that do not specify an icon will fall back to the icon of the action mod. To specify an icon, add an "icon": "path/to/icon.svg" property to the script in the manifest.

Visualization mods

Version 2.1 of the API also contains some smaller improvements to visualization mods.

Axis expression heuristics

When a new instance of a visualization mod is created, heuristics are used to assign initial expressions to the axes of the visualization. You can now specify a string, like count(), to use as the initial expression. This is made in the automaticConfiguration/expressionHeuristics property of an axis specification in the manifest.

When the default heuristics are used for a continuous axis, the allowNonAggregatingMeasures can be set to "prefer" to control the heuristics to attempt to select an expression that is not aggregating.

More styling information

The StylingInfo object has been extended with more information describing the current theme. For instance regarding how zoom sliders and trellis panel headers are styled.

Last modified April 7, 2026