Create a Custom Visualization in TIBCO Spotfire®

Last updated:
2:52am Dec 21, 2020

Back to C# extensions overview

Note: As of Spotfire 11, the core product now features Spotfire Mods, a JavaScript based framework that allows developers to quickly build new resuable interactive Spotfire visualizations. More information here: TIBCO Spotfire® Mods.

Introduction

The set of Spotfire visualizations can be extended. Custom visualizations that have been deployed in the Spotfire environment appear in the same places and behave in the same way as Spotfire visualizations: They are accessible from the visualization menu, from the plot context menus and from the toolbar, and they can share data sets, marking and filtering with other plots.

Prerequisites

  • TIBCO Spotfire® Developer (SDK), see download instructions here.
  • TIBCO Spotfire® Analyst, download from edelivery.tibco.com.
  • Microsoft Visual Studio® 2013 or higher. The free Community edition is available for download here.

See also

Extension points

The extension points for the visualization framework are:

The custom class derived from CustomVisualFactory needs to be registered with the RegisterVisuals method on the AddIn class togeheter with a CustomTypeIdentifier. The view class derived from CustomVisualView is registered with the RegisterViews method.

Implementation

Creating the model

The model part inherits from CustomVisual or CustomVisualization. CustomVisualization is a subclass of CustomVisual and adds functionality for specifying an active data table and an active marking. Being able to specify an active data table and an active marking enables visualizations to participate in master detail scenarios. It also enables the framework to implement commands on marked records and show information about the number of filtered and marked rows in the status bar.

The model is typically derived from CustomVisualization if the visualization visualizes data from one or more data tables, and CustomVisual if it doesn't, which will be the case case when implementing something in line with the built-in Text Area or some sort of control panel to be embedded in a page.

A visualization is expected to be capable of rendering itself. It is also expected to be able to notify the environment when its appearance has changed and it needs to be redrawn. The model achieves this dual requirement by overriding two virtual methods defined by CustomVisual:

The RenderCore method performs the drawing.

The GetRenderTriggerCore returns a trigger that fires whenever the visualization shall be redrawn.

The following sample shows a minimal visualization. It has two properties, representing a data table and a marking. When asked to render itself, it just draws a string specifying the number of marked rows in the data table.

[Serializable]
[PersistenceVersion(1, 0)]
public class MyVisualization : CustomVisualization
{
    // Property names
     
    public new abstract class PropertyNames : CustomVisualization.PropertyNames
    {
        public static readonly PropertyName DataTableReference = CreatePropertyName("DataTableReference");
        public static readonly PropertyName MarkingReference = CreatePropertyName("MarkingReference");
    }
 
    // Fields
     
    private readonly UndoableCrossReferenceProperty<DataTable> dataTableReference;
    private readonly UndoableCrossReferenceProperty<DataMarkingSelection> markingReference;
 
    // Constructor
     
    internal MyVisualization()
    {
        CreateProperty(PropertyNames.DataTableReference, out this.dataTableReference, null);
        CreateProperty(PropertyNames.MarkingReference, out this.markingReference, null);
    }
 
    // Properties
 
    public DataTable DataTableReference
    {
        get { return this.dataTableReference.Value; }
        set { this.dataTableReference.Value = value; }
    }
 
    public DataMarkingSelection MarkingReference
    {
        get { return this.markingReference.Value; }
        set { this.markingReference.Value = value; }
    }
 
    // Rendering
 
    protected override void RenderCore(RenderArgs renderArgs)
    {
        // Clear our drawing surface.
        renderArgs.Graphics.FillRectangle(Brushes.White, renderArgs.Bounds);
 
        // Draw a string showing the number of marked rows.
        if (this.DataTableReference != null && this.MarkingReference != null)
        {
            int markedRowsCount = this.MarkingReference.GetSelection(this.DataTableReference).IncludedRowCount;
            string message = string.Format(
                "{0} marked rows in table {1}",
                markedRowsCount,
                this.DataTableReference.Name);
 
            renderArgs.Graphics.DrawString(message,
                SystemFonts.DefaultFont,
                Brushes.Black,
                renderArgs.Bounds);
        }
    }
 
    protected override Trigger GetRenderTriggerCore()
    {
        // Return a trigger that fires when the data table or marking properties change
        // or when the content of the marking changes.
        return Trigger.CreateCompositeTrigger(
            Trigger.CreatePropertyTrigger(
                this, PropertyNames.DataTableReference, PropertyNames.MarkingReference
            ),
            Trigger.CreateMutablePropertyTrigger<DataMarkingSelection>(
                this, PropertyNames.MarkingReference, DataSelection.PropertyNames.Selection
            )
        );
    }
 
    // Overrides for active data table and active marking.
 
    protected override DataTable GetActiveDataTableReferenceCore()
    {
        return this.dataTableReference.Value;
    }
 
    protected override Trigger GetActiveDataTableReferenceTriggerCore()
    {
        return Trigger.CreatePropertyTrigger(this, PropertyNames.DataTableReference);
    }
 
    protected override DataMarkingSelection GetActiveMarkingReferenceCore()
    {
        return this.markingReference.Value;
    }
 
    protected override Trigger GetActiveMarkingReferenceTriggerCore()
    {
        return Trigger.CreatePropertyTrigger(this, PropertyNames.MarkingReference);
    }
 
    // Serialization
 
    private MyVisualization(SerializationInfo info, StreamingContext context) : base(info, context)
    {
        DeserializeProperty(info, context, PropertyNames.DataTableReference, out this.dataTableReference);
        DeserializeProperty(info, context, PropertyNames.MarkingReference, out this.markingReference);
    }
 
    protected override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        base.GetObjectData(info, context);
 
        SerializeProperty(info, context, this.dataTableReference);
        SerializeProperty(info, context, this.markingReference);
    }
}

 

Creating a Factory

In order for a visualization to be creatable, a custom factory must be specified for it. The factory is responsible for creation and initial configuration of the visualization. It also holds some metadata, such as an image to use for UI commands and a type identifier.

public sealed class MyCustomIdentifiers : CustomTypeIdentifiers
{
    public static readonly CustomTypeIdentifier MyVisualizationIdentifier =
        CreateTypeIdentifier(
            "Acme.MyVisualization",    // Name
            "My Visualization",        // Display name
            "This is a description");  // Description
}
 
internal sealed class MyVisualizationFactory : CustomVisualFactory<MyVisualization>
{
    internal MyVisualizationFactory()
        : base(
        MyCustomIdentifiers.MyVisualizationIdentifier,    
        VisualCategory.Visualization,                  
        Properties.Resources.MyVisualizationImage,     
        null)                                          
    {
    }
 
    protected override void AutoConfigureCore(MyVisualization visualization)
    {
        // Find good default values for properties.
        DataManager dataManager = visualization.Context.GetService<DataManager>();
        visualization.DataTableReference = dataManager.Tables.DefaultTableReference;
        visualization.MarkingReference = dataManager.Markings.DefaultMarkingReference;
    }
}

 

The visualization factory is derived from CustomVisualFactory. Note that the custom factory class does not actually instantiate the visualization. It is the factory base class that instantiates the visualization through the parameterless constructor of the visualization. The constructor does not have to be public; in fact, it should not be public, since there is no way for an API user to create a visualization and then insert it into the document.

When the visualization has been created and inserted into the document node hierarchy, the framework calls two virtual methods on the factory: InitializeCore, which sets properties that are not directly related o data, and AutoConfigureCore, which configures the visualization with appropriate default values for data related properties, such as the data table, marking and columns to use. In the example above, only the AutoConfigureCore method overridden.

Register the Visualization

Finally, the visualization factory must be registered with the framework. This is performed from the RegisterVisualsoverride in the add-in.

public sealed class MyVisualizationAddIn : AddIn
{
    protected override void RegisterVisuals(AddIn.VisualRegistrar registrar)
    {
        registrar.Register(new MyVisualizationFactory());
    }
}

 

The visualization can now be created in TIBCO Spotfire, and displayed in the Analyst, Business Author and Consumer clients. It will also be able o take part in printing and export scenarios. There is, however, no way to configure the visualization or interact with it in any way. In order to do that, a view must be created for the visualization.

Creating the View

The custom visual view API is a unified API for creating custom visualizations in Spotfire applications. With the modified architecture in TIBCO Spotfire 7.5 comes the benefit of code sharing. Instead of maintaining one piece of customization code in TIBCO Spotfire Analyst and another one in TIBCO Spotfire Consumer and Business Author, it is now recommended to only use the web-based approach. A web-based custom visualization will be embedded in TIBCO Spotfire Analyst much like any internal visualization.

If you want any previously created custom visualizations to work with the new API, they need to be converted. See Convert a custom web visualization to the CustomVisualView API for more information.

A custom view class is derived from the base class CustomVisualView<T>. In the AddIn class the view is registered in the RegisterViews method, for example:

protected override void RegisterViews(ViewRegistrar registrar)
{
      base.RegisterViews(registrar);
      registrar.Register(typeof(CustomVisualView), typeof(MyVisual), typeof(MyVisualView));
}

 

Initialize the Visualization

An intance of CustomVisualView can be seen as an embedded web server that typically provides an html file, together with some resources such as JavaScript files, images, etc. 

To initialize the visualization, override the GetResourceCore method:

protected override HttpContent GetResourceCore(string path, NameValueCollection query, MyVisual snapshotNode)
{
    if (string.IsNullOrEmpty(path))
    {
        path = "MyVisual.html";
    }

    var bytes = GetEmbeddedResource("SpotfireDeveloper.CustomVisualsExample.webroot." + path);
    return new HttpContent("text/html", bytes);
}

 

Technically, when the end user creates a visualization of your custom type, what will happen internally is that an "embedded web client" (an iFrame html element) will appear on the current page, and that web client will make an http request with the URL "/". That request will be routed to call the above method GetResourceCore. In the example above, MyVisual.html will be retrieved. That html file may in turn load any resource:

<script src=”http://code.jquery.com/jquery-1.11.2.min.js”></script>
<script src=”myscript.js”></script>

 

The request for myscript.js is specified with a relative path so it will be handled by the overridden GetResourceCore method. In the example above, the location of the JavaScript file in the webroot folder is looked up and the file is embedded. However, the JavaScript file could be fetched in any fashion. For example, in the case of debugging, it could be useful to fetch the file from a hardcoded path to avoid a rebuild of the project.

Client-driven Interaction

Your custom visualization can be regarded as if it is divided into a client side and a server side, where the client side is a "view" and the server side is a "model".

Some utilities are provided to simplify communication with the server.

Reading Data

When the "SpotfireLoaded" event is triggered, the communication channel is open between the client and the server. In this example, the Spotfire.read function fetches data from the server.

$(window).on("SpotfireLoaded", function()
    {
        Spotfire.read("GetData", {"argument": "value"}, function(data)
        {
            if (data)
            {
                // typically re-render the visualization, but this example
                // just displays the returned value
                alert(data);
            }
        });
    });

 

The Spotfire.read function takes three arguments:

  • A method identifier
  • An argument object
  • A callback

On the server side, the read call is handled by the ReadCore method:

protected override string ReadCore(string method, string args, MyVisual snapshotNode)
{
    if (method.Equals("GetData", StringComparison.OrdinalIgnoreCase))
    {
        // typically retrieve some data from the document, 
        // but this example just echoes the input argument
        return args;
    }
        
    return base.ReadCore(method, args, snapshotNode);
}

 

Note that when reading data, the serving code is executed on a background thread. This allows multiple simultaneous read operations.

Writing Data

When the client needs to modify the document, for example, when data is marked, use:

Spotfire.modify("Mark", {'rectangle': someObject});

On the server side, the modify call is handled by the ModifyCore method:

protected override void ModifyCore(string method, string args, MyVisual liveNode)
    {
        if ("Mark".Equals(method, StringComparison.Ordinal))
        { 
            liveNode.MarkArea(/* … */);
        }
    }

 

Note that the modification of the document is performed on the main thread, so it is safe to modify the document.

Server-driven interaction

From the server, it is possible to trigger event handlers on the client side, for example:

protected override void OnUpdateRequiredCore()
{
    this.InvokeClientEventHandler("render", null);
}

 

For more information, see OnUpdateRequiredCore.

This code will run when the client must be updated, by triggering the “render” event.

At the client:

var render = function(data)
{                
    // Typically call Spotfire.read(...)
    // data will be the argument from the server invocation - null in this example
};

Spotfire.addEventHandler("render", render);

 

Render custom visuals when exporting

There are some things to consider in order to make a custom visual render nicely when exporting with the new export framework that was introduced in 7.10. Learn more here.