Reading time: 8 minutes and 45 seconds.
Tutorial
1. Initial setup
- Create a folder called
setup-analysis
and open it in Visual Studio Code (VS Code). - Launch a terminal in VS Code and run
npx @spotfire/mods-sdk@latest new action
followed bynpm install
. - Launch the default build task (Ctrl+Shift+B).
- Connect to the mod in Spotfire and click Start debugging.
2. Validate our development environment
The first step is to make sure that we have a working development environment. We want to make sure that mods can be debugged and that the executed script code is updated when we make changes.
- Go to the Test tab in the Mods development tool.
- If script output is hidden, click Show script output.
- To run the script:
- Click Test run.
- Click Next in the flyout that opens.
- Give the configuration the name “Test 1”, so that we can re-use the configuration later.
- For the message parameter, enter “tutorial”.
- Click Run.
- “tutorial!” should now be printed in the script output.
To check that we can debug the script, and that our local changes are picked up by Spotfire, do the following:
-
Open
my-script.ts
in VS Code and prepend"Hello "
to the message by changing line 8 to:const message = addExclamationMark("Hello " + _message);
-
Put a breakpoint on line 8 by clicking the red dot to the left of the line number (visible when hovering).
-
Press F5 and enter the debug port shown in the Mods development tool in Spotfire (default is 9222) to connect to the debugger.
-
Run the “Test 1” configuration. VS Code should now be stuck on line 8, and Spotfire should be showing a progress dialog. To inspect variables, you can hover over them. For example, hover over
_message
to see the message that was entered during configuration.- Press F11 (Step into) to follow the code into the
addExclamationMark
function. - Press F10 (Step over) to jump to the next line.
- Press F5 (Continue) to resume the script execution until finished or until reaching another breakpoint.
For more information on what is possible to do when debugging in VS Code, refer to Debugging in Visual Studio Code.
- Press F11 (Step into) to follow the code into the
-
Resume execution of the script (press F5). “Hello tutorial!” should be visible in the script output.
3. Set up document properties
In Spotfire it is common to store analysis specific values in document properties. We can create these properties using the APIs DataProperty.CreateCustomPrototype to create a property object and DataPropertyRegistry.AddProperty to add the object to the document. The snippet “Set a Document Property” shows how to set or create a document property parameterized by name, type, and value. However, for the purposes of this tutorial, we will assume that we know what document properties to create, and that the script is running in a fresh analysis.
We will create the following document properties:
- a property to store the name of our company.
- a property to store the date the analysis was created.
Let’s start by creating a new script by running npx @spotfire/mods-sdk add-script setup-analysis --name "Sets up the analysis"
.
A file named setup-analysis.ts
should now have been created, and the mod-manifest.json
file have been updated with an entry for the script (remember to reload the manifest in the Mods development tool before you run the script). Open the file and change the code to look like this:
// Create shorthands for the relevant APIs.
const { DataProperty, DataType, DataPropertyClass } = Spotfire.Dxp.Data;
export function setupAnalysis({ document, application }: SetupAnalysisParameters) {
// Check if the property already exists.
if (document.Data.Properties.ContainsProperty(DataPropertyClass.Document, "companyName")) {
return;
}
// Create the property object.
const property = DataProperty.CreateCustomPrototype("companyName", DataType.String, DataProperty.DefaultAttributes);
// Set the property value.
property.Value = "Tutorial Company";
// Add it to the document.
document.Data.Properties.AddProperty(DataPropertyClass.Document, property);
}
RegisterEntryPoint(setupAnalysis);
If you run this script, a document property with the name “companyName” will be created.
The code for creating the second property will be pretty much identical except the property name, type, and initial value will be different.
We can re-use this code by creating a function within the script closure which is parameterized by name and type.
The setupAnalysis
function should now look like this:
// Create shorthands for the relevant APIs.
const { DataProperty, DataType, DataPropertyClass } = Spotfire.Dxp.Data;
export function setupAnalysis({ document, application }: SetupAnalysisParameters) {
addProperty("companyName", DataType.String, "Tutorial Company");
addProperty("creationDate", DataType.DateTime, System.DateTime.Now);
function addProperty(name: string, type: Spotfire.Dxp.Data.DataType, value: unknown) {
// Check if the property already exists.
if (document.Data.Properties.ContainsProperty(DataPropertyClass.Document, name)) {
return;
}
// Create the property object.
const property = DataProperty.CreateCustomPrototype(name, type, DataProperty.DefaultAttributes);
// Set the property value.
property.Value = value;
// Add it to the document.
document.Data.Properties.AddProperty(DataPropertyClass.Document, property);
}
}
RegisterEntryPoint(setupAnalysis);
If we wanted to re-use the addProperty
function across multiple scripts in the action mod, we could move it to the utils.ts
file and import it in the same way that addExclamationMark
is imported in my-script.ts
.
Moving the function would require adding a document
parameter with type Spotfire.Dxp.Application.Document
to the function.
4. Load data from the library
Spotfire has APIs for interacting with the library, such as saving the analysis to the library via AnalysisApplication.SaveAs and AnalysisApplication.SaveCopy, or exporting a data table to the library via DataTable.ExportDataToLibrary. These APIs generally take or return a LibraryItem representing the entry in the library. Each item is uniquely defined by an ID, known as a GUID. We can retrieve the relevant LibraryItem from a GUID via the LibraryManager.TryGetItem method, or by searching for the item via LibraryManager.Search.
If you do not have an SBDF file saved to the library, you can simply export the currently loaded data to the library.
We will assume that the SBDF file is named “my-data” in the rest of the tutorial.
Try to search for the item in the library by updating setup-analysis.ts
to look like this:
// Create shorthands for the relevant APIs.
const { DataType, DataProperty, DataPropertyClass } = Spotfire.Dxp.Data;
const { LibraryManager, LibraryItemRetrievalOption } = Spotfire.Dxp.Framework.Library;
export function setupAnalysis({ document, application }: SetupAnalysisParameters) {
addProperty("companyName", DataType.String, "Tutorial Company");
addProperty("creationDate", DataType.DateTime, System.DateTime.Now);
// Retrieve the library manager service and assume it is defined since we are connected to a TSS.
const libraryManager = application.GetService(LibraryManager)!;
const results = libraryManager.Search("type::sbdf my-data", LibraryItemRetrievalOption.IncludePath);
for (const result of results) {
console.log(`Found item: Title=${result.Title}, Path=${result.Path}, GUID=${result.Id.ToString()}`);
}
function addProperty(name: string, type: Spotfire.Dxp.Data.DataType, value: unknown) {
if (document.Data.Properties.ContainsProperty(DataPropertyClass.Document, name)) {
return;
}
// Create a the property object
const property = DataProperty.CreateCustomPrototype(name, type, DataProperty.DefaultAttributes);
// Set the property value.
property.Value = value;
// Add it to the document
document.Data.Properties.AddProperty(DataPropertyClass.Document, property);
}
}
RegisterEntryPoint(setupAnalysis);
Running this script will result in an error that looks something like this:
Could not run action
The API function 'LibraryItemRetrievalOption.get_IncludePath' requires 'LibraryRead' to be specified in the "capabilities" property of the manifest file.
Error: The property get method is unavailable or inaccessible
at setupAnalysis (C:\setup-analysis\build\setup-analysis.js:10:92) -> const results = libraryManager.Search("type::sbdf my-data", LibraryItemRetrievalOption.IncludePath);
The error indicates that we need to declare in the mod manifest that we intend to interact with the library. Action mods are designed to be safe to use, and require that the mod declares when it wants to use APIs that interact with systems outside the current analysis. This declaration is known as a capability of the mod, and will show up in trust dialogs when the user is asked to trust the mod. For more information, see “What are capabilities?”.
Update the mod-manifest.json
file by adding "LibraryRead"
to the "capabilities"
array.
Making changes to the manifest file requires reloading of the mod, which can be done by clicking Reload manifest in the Mods development tool.
If you run the script again, the debug output should now display a search result for the SBDF file:
Found item: Title=my-data, Path=/my-data, GUID=a82c6a33-2743-492d-870b-8c188f8cc66b
Now that our search results in a match, we can update the script to load data. One option is to use the search result:
const myDataItem = Array.from(results).find(i => i.Title === "my-data" && i.ItemType === LibraryItemType.SbdfDataFile);
Another option is to use the GUID (this method uses an output parameters, see “How do I use APIs with output parameters?” for more info):
const myDataItem = OutParam.create(Spotfire.Dxp.Framework.Library.LibraryItem);
libraryManager.TryGetItem(System.Guid.Parse("a82c6a33-2743-492d-870b-8c188f8cc66b"), myDataItem.out);
Using the library item we can load the data into the analysis using the SbdfLibraryDataSource and DataTableCollection.Add APIs:
const sbdfDataSource = new SbdfLibraryDataSource(myDataItem);
document.Data.Tables.Add(tableName, sbdfDataSource);
Similarily to the addProperty
function, we can move this logic out into a separate function in case we want to re-use the logic with a different library item in the future.
With the option using the search result, the script setup-analysis.ts
should now look like this:
// Create shorthands for the relevant APIs.
const { DataType, DataProperty, DataPropertyClass } = Spotfire.Dxp.Data;
const { SbdfLibraryDataSource } = Spotfire.Dxp.Data.Import;
const { LibraryManager, LibraryItemRetrievalOption, LibraryItemType } = Spotfire.Dxp.Framework.Library;
export function setupAnalysis({ document, application }: SetupAnalysisParameters) {
addProperty("companyName", DataType.String, "Tutorial Company");
addProperty("creationDate", DataType.DateTime, System.DateTime.Now);
loadLibraryData("my-data");
function loadLibraryData(tableName: string) {
if (document.Data.Tables.Contains(tableName)) {
return document.Data.Tables.Item.get(tableName)!;
}
// Retrieve the library manager service and assume it is defined since we are connected to a TSS.
const libraryManager = application.GetService(LibraryManager)!;
const results = libraryManager.Search(`type::sbdf ${tableName}`, LibraryItemRetrievalOption.IncludePath);
const myDataItem = Array.from(results).find(
(i) => i.Title === "my-data" && i.ItemType === LibraryItemType.SbdfDataFile,
);
if (!myDataItem) {
throw new Error(`Found no SBDF in the library with the name '${tableName}'.`);
}
// Load the data.
const sbdfDataSource = new SbdfLibraryDataSource(myDataItem);
return document.Data.Tables.Add(tableName, sbdfDataSource);
}
function addProperty(name: string, type: Spotfire.Dxp.Data.DataType, value: unknown) {
if (document.Data.Properties.ContainsProperty(DataPropertyClass.Document, name)) {
return;
}
// Create a the property object
const property = DataProperty.CreateCustomPrototype(name, type, DataProperty.DefaultAttributes);
// Set the property value.
property.Value = value;
// Add it to the document
document.Data.Properties.AddProperty(DataPropertyClass.Document, property);
}
}
RegisterEntryPoint(setupAnalysis);
5. Add a visualization
Let’s finish this tutorial off by adding a visualization to the current page, and configure it to use the data we loaded from the library.
Visualizations can be added to a page via the VisualCollection.AddNew API.
We will modify loadLibraryData
slightly, to return a reference to the data table, and then set it as the VisualizationData.DataTableReference for our newly created bar chart visualization.
After setting the data table reference Visual.AutoConfigure can be used to automatically set up the axis, after which we will change the bar chart to be horizontal and colored by the X-axis expression.
The script setup-analysis.ts
should now look like this:
// Create shorthands for the relevant APIs.
const { DataType, DataProperty, DataPropertyClass } = Spotfire.Dxp.Data;
const { SbdfLibraryDataSource } = Spotfire.Dxp.Data.Import;
const { LibraryManager, LibraryItemRetrievalOption, LibraryItemType } = Spotfire.Dxp.Framework.Library;
const { VisualTypeIdentifiers, BarChart, BarChartOrientation } = Spotfire.Dxp.Application.Visuals;
export function setupAnalysis({ document, application }: SetupAnalysisParameters) {
addProperty("companyName", DataType.String, "Tutorial Company");
addProperty("creationDate", DataType.DateTime, System.DateTime.Now);
const myDataTable = loadLibraryData("my-data");
let page = document.ActivePageReference;
if (!page) {
// No pages exist, add one.
page = document.Pages.AddNew();
}
const barChart = page.Visuals.AddNew(VisualTypeIdentifiers.BarChart).As(BarChart)!;
barChart.Title = "Tutorial Visualization";
barChart.Data.DataTableReference = myDataTable;
// Use AutoConfigure to setup axis.
barChart.AutoConfigure();
barChart.Orientation = BarChartOrientation.Horizontal;
barChart.ColorAxis.Expression = barChart.XAxis.Expression;
function loadLibraryData(tableName: string) {
if (document.Data.Tables.Contains(tableName)) {
return document.Data.Tables.Item.get(tableName)!;
}
// Retrieve the library manager service and assume it is defined since we are connected to a TSS.
const libraryManager = application.GetService(LibraryManager)!;
const results = libraryManager.Search(`type::sbdf ${tableName}`, LibraryItemRetrievalOption.IncludePath);
const myDataItem = Array.from(results).find(
(i) => i.Title === "my-data" && i.ItemType === LibraryItemType.SbdfDataFile,
);
if (!myDataItem) {
throw new Error(`Found no SBDF in the library with the name '${tableName}'.`);
}
// Load the data.
const sbdfDataSource = new SbdfLibraryDataSource(myDataItem);
return document.Data.Tables.Add(tableName, sbdfDataSource);
}
function addProperty(name: string, type: Spotfire.Dxp.Data.DataType, value: unknown) {
if (document.Data.Properties.ContainsProperty(DataPropertyClass.Document, name)) {
return;
}
// Create a the property object
const property = DataProperty.CreateCustomPrototype(name, type, DataProperty.DefaultAttributes);
// Set the property value.
property.Value = value;
// Add it to the document
document.Data.Properties.AddProperty(DataPropertyClass.Document, property);
}
}
RegisterEntryPoint(setupAnalysis);