Chart State : Importing and Exporting

When a user closes an application and comes back to it, they expect to pick up right where they left off. Given the infinite combinations of drawings, technical indicators, and chart types, state management could be a complex process. Thankfully, the ChartIQ library provides several methods that make this task relatively simple. Before we talk about that, let's introduce some terminology.

Terminology

The user has considerable control over the chart's look and feel. All of that information is stored in two objects: layout and drawings.

  • Layout - stxx.chart.layout is an object that contains all settings that affect how data is represented on the chart (i.e. chart type, zoom level, periodicity, studies, range, etc). The layout is independent of the currently loaded symbol. For instance, if the chart is set to display candles, it will remain a candle chart when the symbol is changed. The same is true of everything that makes up the layout object.

    Use the CIQ.ChartEngine#importLayout and CIQ.ChartEngine#exportLayout to save and restore the layout.

    Example, a look inside a typical layout :

screenshot of  the layout object of a sample chart

  • Drawings - Drawings are associated with a particular symbol. A chart contains an array of drawings for the given symbol. The array changes when a new symbol is loaded. (The chart does not remember drawings for all symbols the user has examined!)

    Drawings are represented as points of "time" and "value" (price). Drawings work across periodicities. For instance, if a user places a channel drawing on AAPL 5 minute chart, the drawing will still show when the user switches to a 30 minute chart - it will just be smaller.

    Use the CIQ.ChartEngine#importDrawings and CIQ.ChartEngine#exportDrawings to save and restore drawings.

Saving and Restoring: Example Code

The default template ("template-advanced.html") saves all information in the browser's localStorage in real time; however, developers can choose to store layout and drawings wherever they want (for instance, in a remote database). Below are a few examples that demonstrate saving and loading chart data using both localStorage and using a remote server. Later, we'll put it all together so that data is saved every time a user makes a change to the chart.

Example, saving to localStorage :

    //assumes global chart object called stxx
    function saveLayout(){
        var layout=JSON.stringify(stxx.exportLayout(true));
        localStorage.setItem('myChartLayout', layout);
    }

    function saveDrawings(){
        var drawings=JSON.stringify(stxx.exportDrawings());

        //saves drawings with symbol as the key
        localStorage.setItem(stxx.chart.symbol, drawings);
    }

Example, saving to a remote database :

    // This example assumes a global chart object called "stxx" and global "username"

    function saveLayout(){
        var layout=stxx.exportLayout(true);

        jQuery.ajax({
            method: "POST",
            url: 'https://myserver.com/saveLayout',
            data:{
                "layout": JSON.stringify(layout),
                "username": username
            }
        });
    }

    function saveDrawings(){
        var drawings=stxx.exportDrawings();
          var payload={
            symbol: stxx.chart.symbol,
            drawings: JSON.stringify(drawings),
            user: username
        };

        jQuery.ajax({
            method: "POST",
            url: 'https://myserver.com/saveDrawings',
            data: payload
        });
    }

Example, restoring from localStorage :

    //This example assumes a global chartObject called stxx

    function restoreLayout(){
        var savedLayout=localStorage.getItem('myChartLayout');
        var importOptions={
            managePeriodicity:true,
            preserveTicksAndCandleWidth:true
        };

        if(!savedLayout) return; // nothing to restore
        stxx.importLayout(savedLayout, importOptions);
    };

    function restoreDrawings(){
        var drawings=localStorage.getItem(stxx.chart.symbol);

        if(!drawings) return; // nothing to restore
        stxx.importDrawings(drawings);
    };

Example, restoring from a remote database :

    function restoreLayout(){
        //declare all necessary variables for the ajax request.
        var importOptions={
            managePeriodicity:true,
            preserveTicksAndCandleWidth:true
        };

        var successCallback=function(response){
            try{
                //parse your response
                var savedLayout=JSON.parse(response);
                if(savedLayout) stxx.importLayout(savedLayout, importOptions);
            }catch(e){}
        };

        var errorCallback=function(jqXHR, textStatus){
            alert('Request Failed: ' + textStatus);
        };

        var request = jQuery.ajax({
            method: "POST",
            url:'https://myserver.com/getLayout/username?username=' + username;
        });
        request.done(successCallback);
        request.fail(errorCallback);
    };

    function restoreDrawings(){
        var successCallback=function(response){
            try{
                var drawings=JSON.parse(response);
                if(drawings) stxx.importDrawings(drawings); //import drawing
            }catch(e){}
        };

        var errorCallback=function(jqXHR, textStatus){
            alert('Request Failed: ' + textStatus);
        };

        //assumes a method on the server that returns drawings for a username/symbol key.
        var request=jQuery.ajax({
            url:'https://myserver.com/getDrawingsByUser/username?username=' + username + '&symbol=' + stxx.chart.symbol;
        });
        request.done(successCallback);
        request.fail(errorCallback);
    };

Sidebar: Options for importLayout() and exportLayout()

importLayout()

managePeriodicity - set this parameter to true when you want to import periodicity (i.e. 1 minute bars) and/or range (i.e. 1 year chart) from a saved layout. If you don't set this, then the default (or current) periodicity and range will be maintained.

preserveTicksAndCandleWidth - If true, then the current chart zoom level (candleWidth) will be maintained. If set to false then the zoom level will be restored from the saved layout. Note: To ensure a reasonable candle size is available across devices, the candle width will be reset to 8px if larger than 50px. By default, the candleWidth is not restored from a saved layout.

exportLayout()

withSymbols - If this parameter is true, all of a chart's symbols (main symbol and any comparisons) will be serialized with the layout. Setting "withSymbols" to true essentially treats exporting as saving a "chart" rather than saving a "view". This is the most convenient way to restore the last used stock symbol when a user comes back to your application. Note: If you use exportLayout() with "withSymbols" your code should probably not be calling stxx.newChart()!

Putting it all together: Chart callbacks

To make sure that state is saved whenever the user interacts with the chart, you must set three callbacks (in addition to restoring layout on app initialization):

stxx.callbacks.layout=yourSaveLayoutFunction(); // invoked when layout has changed - save layout

stxx.callbacks.drawings=yourSaveDrawingsFunction(); // invoked when drawing added/removed - save drawings

stxx.callbacks.symbolChange=yourRestoreDrawingsFunction(); // invoked when symbol changed - restore the drawings

Advanced Considerations

For most implementations, the above examples will be more than adequate to maintain a consistent user experience. There are two notable exceptions:

  • Running your application in multiple windows, and
  • Running your application on multiple devices.

Multiple Charts In One Window

Each chart's state is represented by its "layout" object. Saving and restoring state for more than one chart on a single screen is sometimes called saving a workspace. To save a workspace, call exportLayout() for each chart and save each layout to a workspace object. Then persist that object.

Here we use a convenient trick, we assume that the "id" for each chart's container element is uniquely named:

var workspace={};
for(var i=0;i<myCharts.length;i++){ // assumes you have an array of ChartEngine called myCharts
  var chartEngine=myCharts[i];
  workspace[chartEngine.container.id]=chartEngine.exportLayout();
  // maybe save other stuff like the position or dimensions of the chart container?
}
localStorage.put("workspace", JSON.stringify(workspace));

Multiple Windows/Tabs

The sample code in template-advanced.html assumes that an application will store the chart's layout using a standard name (e.g., myChartLayout). This means that if you open multiple instances of your application, they will all save data to the same place (potentially overwriting each other). This is typically fine - most web applications assume that the user won't load multiple instances. But if you have multiple instances of the same application running on the same system (e.g., multiple tabs), the last layout change a user makes will override any prior changes. To avoid this, you will need to develop a system to keep track of which layouts belong to which windows.

Multiple Devices

The idea of a responsive app is simple: build a single code base, and let the UI react to the environment running the code. While there are many positives to responsive apps, they do require extra effort for saving and restoring state.

LocalStorage is tied to a specific browser on a specific device, so changes made on one device will not propagate to the other. If you'd like for data to synchronize across devices, you must save your user's data to a remote database.

We recommend using a separate key (e.g., "myWebLayout" vs “myMobileLayout") for the mobile and web implementations. While mobile implementations are limited to a smaller screen, web applications will most likely have access to a full 20"+ monitor. Users on larger screens will often crowd a chart with many studies. Displaying that amount of information on a mobile device is problematic. By using separate keys for mobile and web, your application can load the appropriate layout for the user's device, and you will avoid that problem.

Note: If you set up multiple keys but still want to give users access to both mobile and web layouts, you should consider implementing a menu that simply calls either stxx.importLayout(myWebLayout) or stxx.importLayout(myMobileLayout), depending on the user's preference.

Notes

  • importLayout() and exportLayout() save and restore layout as JavaScript objects (pojo). To save to a remote database you should convert the layout object to and from JSON.

  • Safari does not retain localStorage in iframes from a different domain than the parent. To enable local storage functionality in this circumstance, direct your users to click the "Safari" menu and go to "Preferences". Under the "Privacy" tab select "Always allow" for "Cookies and website data".

    Safari Privacy


Next Steps:

  • [Series, Comparisons and Renderers: An explanation of the addSeries() method] Renderers