Jump to content
  • Create a Custom Visualization in Spotfire®


    This article explains when to use Spotfire Mods, and when to use a Custom Visualization?

    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 reusable interactive Spotfire visualizations. More information here: Spotfire® Mods.

    When should I use Spotfire Mods, and when should I use a Custom Visualization?

    • When external data (i. e. in-db queries to the database with Spotfire Data Connectors) is required, Mods would be the option as the C# Custom Visual framework does not currently support external data.
    • When the rendering is too complicated, or the data required for the visualization is too complicated (i.e. multiple tables, extremely large amount of data points, need for image rendering in C#) and the data is managed by the Spotfire in-memory data engine, a C# Custom Visualization extension would be the right choice.
    • If there are no constraints for either above, Mods, will give the most seamless experience with the overall user experience of Spotfire, and is the recommended path moving forward.

    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

    • Spotfire® Developer (SDK), and 
    • Spotfire® Analyst, both available as downloads 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 together 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 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 Spotfire 7.5 comes the benefit of code sharing. Instead of maintaining one piece of customization code in Spotfire Analyst and another one in 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 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.


    User Feedback

    Recommended Comments

    There are no comments to display.


×
×
  • Create New...