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 three objects: layout, preferences and drawings.

  • Layout - stxx.layout is an object that contains all settings that affect what data the user wants to see on the chart and how (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

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.loadChart()!

  • Preferences - stxx.preferences is an object that contains additional visualization settings defined by the firm, which can not be modified by the user (i.e. Display Current Price Line, Allow Line Dragging, Define Highlights Radius, Display Study Floating Labels, Magnetize White Drawing , Define Zoom Speed, etc). The preferences are independent of the currently loaded symbol.

    Use the CIQ.ChartEngine#importPreferences and CIQ.ChartEngine#exportPreferences to save and restore the preferences.

  • 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 ("sample-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 savePreferences() {
	var preferences = JSON.stringify(stxx.exportPreferences());
	localStorage.setItem("myChartPreferences", preferences);
}

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) stxx.importLayout(savedLayout, importOptions);
}

function restorePreferences() {
	var pref = CIQ.localStorage.getItem("myChartPreferences");
	if (pref) stxx.importPreferences(JSON.parse(pref));
}

function restoreDrawings() {
	var drawings = localStorage.getItem(stxx.chart.symbol);
	if (drawings) 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);
}

Putting it all together: Event listeners

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

// invoked when layout has changed - save layout
stxx.addEventListener("layout", yourSaveLayoutFunction);

// invoked when symbol changed - save layout
stxx.addEventListener("symbolChange", yourSaveLayoutFunction);

// invoked when preferences have changed - save preferences
stxx.addEventListener("preferences", yourSavePreferencesFunction);

// invoked when drawing added/removed - save drawings
stxx.addEventListener("drawing", yourSaveDrawingsFunction);

Using the default UI

If you are using the provided templates, you can quickly replace local storage with your own storage location.
Override the following functions with your own:

CIQ.localStorage.getItem=function(name){ 
   //your code here to return the value of 'name' from the source of your choice
};

CIQ.localStorage.removeItem=function(name){ 
   //your code here to remove the 'name' entry from the source of your choice
};

CIQ.localStorage.setItem=function(name,value){ 
   //your code here to store the value of 'name' from the source of your choice
};

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 sample-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: