Markers
Markers provide developers with the ability to superimpose DOM (html) elements, canvas drawings or images directly on the chart to highlight particular time based events. Markers will move with the chart as a user scrolls and zooms, and will be hidden when they pan off of the chart. Markers can be placed on the main chart panel or on study panels.
The following working example includes a variety of different marker rendering styles using multiple HTML templates styled by CSS.
- Click on the
JavaScript
,HTML
andCSS
buttons to see the code for this working sample. - Click on the
Edit in JSFiddle
button to 'play' with this code.
The following is an example of markers on a study panel:
- Click on the
JavaScript
,HTML
andCSS
buttons to see the code for this working sample. - Click on the
Edit in JSFiddle
button to 'play' with this code.
Creating Custom Markers
The mechanism for creating markers is straightforward:
new CIQ.Marker(params);
By default, CIQ.ChartEngine#defaultMarkerPlacement will be used as the marker placement handler. However, developers can override this behavior my declaring a marker placement handler and assigning a corresponding placementFunction
to a marker.
Example:
CIQ.ChartEngine.defaultMarkerPlacement = CIQ.Marker.MyHandler.placementFunction;
Declaring Marker Handlers
Marker Handlers are classes that can be created to manage custom marker functionality.
As a developer, you can create custom handlers derived from CIQ.Marker, or simply utilize the built-in functionality of CIQ.Marker and its defaultMarkerPlacement
function.
Creating a custom Marker Handler allows you to create different marker classes for different event types. For example, you can have a dividend handler that places markers above the candle high, or a news handler that places the marker under the candle low or simply at the bottom of the chart.
The mechanics for creating new marker handler instance are as follows:
1 - Create the function:
CIQ.Marker.MyHandler = function(params) {
if (!this.className) this.className = "CIQ.Marker.MyHandler";
CIQ.Marker.call(this, params);
};
2- Set the inheritance:
CIQ.inheritsFrom(CIQ.Marker.MyHandler, CIQ.Marker, false);
3- Set the placementFunction:
CIQ.Marker.MyHandler.placementFunction = function(params) {
// your placement code here.
};
See CIQ.ChartEngine#defaultMarkerPlacement for more details
Remember that the marker is just a DOM element, so to position it, all you have to do is set its style as you would any other HTML component.
Here is a basic outline for a placement function:
CIQ.Marker.MyHandler.placementFunction = function(params) {
// Iterate through the items to be placed
for (var i = 0; i < params.arr.length; i++) {
var marker = params.arr[i];
var node = marker.node;
//X-axis positioning logic
node.style.left = theXPixel;
//Y-axis positioning logic
node.style.bottom = theYpixel;
}
};
Creating an actual marker
Custom Marker:
myMarker = new CIQ.Marker.MyHandler({
stx: stxx,
xPositioner: "bar",
x: x,
node: document.querySelector("yourHTMLElement")
});
Default Marker:
myMarker = new CIQ.Marker({
stx: stxx,
xPositioner: "bar",
x: x,
node: document.querySelector("yourHTMLElement")
});
x is the horizontal location of the marker. This is determined by the value of xPositioner which can be either:
- "bar" - Bar position on the screen (1st candle is on the left)
- "master" - Position in the masterData (candle will move with the chart)
- "date" - An absolute date. This will be converted to the closest masterData position if the provided date does not exactly match any existing points. Be sure the same timezone as masterData is used.
- "tick" - Represents a position in the dataSet.
- "none" - A raw pixel value. Positive values from left of chart. Negative values from right of chart.
For most implementations, this is all you need to position your markers. See CIQ.Marker for the full list of valid parameters.
Managing markers for nonexistent data points
A marker can only be added to a matching data point. If there is no exact match, it will find the closest date match. To mitigate this, you can always add an invisible placeholder for any bar spacing you want to reserve but not show. You can do this by adding these placeholders on your fetch response before loading the data into the chart, or by calling updateChartData to add the missing points right before you add a marker. Your data object should only include a Date/DT field.
Things to know when adding invisible candles
- You want to add these as full bars, not last sales. 'useAsLastSale' will try to find the right candle given the current periodicity. So that will not add in between periodicities, for example.
- By default, markers are placed above the price point if no specific yPositioner is indicated. But since the invisible placeholders have no price, the markers will not show. Please specify an actual yPositioner that makes sense for the invisible candle, such as yPositioner:"top".
Add placeholder example:
stxx.updateChartData([{ Date: "2023-10-18 00:00" }]);
Use CIQ.ChartEngine#tickFromDate to find out if the element already exists. If the date of the returned tick exactly matches your marker date, there is no need to manually add a datapoint.
Or you can have the chart automatically fill gaps. See: CIQ.ChartEngine#cleanupGaps ( this will require a market definition )
Creating DOM elements for your markers
Although any element can be attached to a marker, you normally don't want to explicitly create each one separately in your DOM. Instead, you should have prototypes which you can then clone, style and attach.
You can create different types of prototypes if you want to have different HTML elements used for different markers. For example, you can create an element with a specific picture or specific style properties like hover or on-click. Then you would clone the appropriate template for the particular marker you want to display.
Assuming the following prototype is present in your DOM:
<div id="stxEventPrototype" class="myEvents">
Here is an example directly cloning an HTML placeholder:
newNode = document.querySelector("#stxEventPrototype").cloneNode(true);
newNode.id = null;
newNode.innerText = "F"; // future tick
newNode.classList.add("dividend");
var someDate = new Date(stxx.masterData[stxx.masterData.length - 1].DT);
someDate = stxx.getNextInterval(someDate, 5); // 5 bars in the future
new CIQ.Marker.MyHandler({
stx: stxx,
xPositioner: "date",
x: someDate,
label: "events",
node: newNode
});
It is very important that you ensure there is a valid and complete CSS class matching the value you use in classList.add()
. Otherwise, the marker may not be rendered if elements are missing. In the above example, newNode.classList.add("dividend");
assigns the 'dividend' class to set the background color for the marker. If missing, no background color will be present (since the myEvents
class does not set one up) and the marker will not be visible.
Creating the marker's CSS
A marker is simply an HTML element that can be styled any way you want to. Therefore, a style sheet or in-line styling is required.
The following is an example of the styles provided in the advanced CSS file:
Note: Absolute positioning is required as the defaultMarkerPlacement function uses left and bottom css properties to position the marker.
.myEvents {
position: absolute;
text-align: center;
width: 20px;
height: 20px;
line-height: 20px;
color: white;
}
.myEvents.dividend {
background-color: blue;
}
.myEvents.news {
background-color: red;
}
.myEvents.earnings {
background-color: purple;
}
A quick way to adjust the placement of a marker, relative to its location, without having to create a custom placement function is to manage it in the CSS. Remember, this is a simple HTML div with corresponding CSS. You can simply navigate to the style declaration and add a margin.
Example:
.myEvents {
position: absolute;
text-align: center;
width: 20px;
height: 20px;
line-height: 20px;
color: white;
margin-bottom: 20px; /*here*/
}
Node Creators as an alternative to explicitly Creating DOM elements
For convenience, ChartIQ offers two types of marker creators to suit different needs.
-
Simple Marker Nodes support the dynamic creation of basic marker nodes such as circles, squares, and callouts.
-
High Performance Marker Nodes can be used in situations where a large number of markers need to be added to the chart. High performance markers are a hybrid between HTML and Canvas.
Note: Simple Markers and Performance Markers use the same parameters. This means you can reuse the default styles that you created for Simple Markers.
Here is an example on how to use the Simple node creator:
function showMarkers() {
var l = stxx.masterData.length;
// An example of a data array to drive the marker creation
var data = [
{
x: l - 5,
type: "circle",
category: "news",
headline: "This is a Marker for a News Item"
},
{
x: l - 15,
type: "square",
category: "earningsUp",
headline: "This is a Marker for Earnings (+)"
},
{
x: l - 25,
type: "callout",
category: "earningsDown",
headline: "This is a Marker for Earnings (-)"
},
{
x: l - 35,
type: "callout",
category: "dividend",
headline: "This is a Marker for Dividends"
},
{
x: l - 45,
type: "callout",
category: "filing",
headline: "This is a Marker for a Filing"
},
{
x: l - 55,
type: "callout",
category: "split",
headline: "This is a Marker for a Split"
}
];
var story =
"Like all ChartIQ markers, the object itself is managed by the chart, so when you scroll the chart the object moves with you. It is also destroyed automatically for you when the symbol is changed.";
// Loop through the data and create markers
for (var i = 0; i < data.length; i++) {
var datum = data[i];
datum.story = story;
var params = {
stx: stxx,
xPositioner: "master",
x: datum.x,
label: "events",
node: new CIQ.Marker.Simple(datum)
};
var marker = new CIQ.Marker(params);
}
stxx.draw();
}
showMarkers()
When using simple node creators, you can include the following line right after you add each marker to have the pop-up enabled by default:
var myMarker = new CIQ.Marker(params);
myMarker.node.classList.toggle("highlight"); // <-- add this line
Node Creators and their CSS
Node Creators allow you a quick way to get started with markers that you can easily customize. CIQ.Marker.Simple and CIQ.Marker.Performance can quickly build DOM nodes and feature a matching set of CSS styles for the nodes to get you started quickly. Using the parameters in the node creator constructor you can add classes to the marker to use either the built-in defaults, or quickly create your own markers.
To extend the node creator default CSS, all you need to do is create a new style declaration in CSS with your new type or category as a class name:
.stx-marker.trade .stx-visual {
background: white;
width: 12px;
height: 12px;
}
This code creates a small white marker category called "trade" that can be easily used with either a "circle" or "square" type
to represent a trade. Since Performance takes the same arguments as Simple markers, you can generally reuse CSS classes between the two if your markers keep the same DOM structure.
Code Example: Displaying Event Markers on the Chart
Once all of the marker placement and styling is in place, it is time to place the markers on the chart.
Here is an example of our helloworld.html
template file, updated to incorporate all the marker customization techniques discussed earlier.
<!doctype html>
<!--
This file provides an example of the most basic way to load a chart into an HTML page. It includes no UI.
-->
<html lang="en-US">
<head>
<title>Hello World</title>
<!-- Set the display for mobile devices. -->
<meta name="viewport" content="width=device-width" />
<!-- Reference the default style sheet. -->
<link rel="stylesheet" type="text/css" href="css/stx-chart.css" media="screen" />
<style>
body {
margin: 0;
}
.myEvents {
position: absolute;
text-align: center;
width: 20px;
height: 20px;
line-height: 20px;
color: white;
}
.myEvents.dividend {
background-color: blue;
}
.myEvents.news {
background-color: red;
}
.myEvents.earnings {
background-color: purple;
}
</style>
</head>
<body>
<!--
Create the chart container. The container can be positioned anywhere on your web page and sized any way you wish,
but the CSS position property must have the value "relative".
-->
<div class="chartContainer" style="width:800px;height:460px;position:relative;"></div>
<div id="stxEventPrototype" class="myEvents"></div>
<script type="module" crossorigin="use-credentials">
// This inline script acts as the entry point, without creating a separate external file.
// Reference a file of statically defined chart data.
import sample5min from "./examples/data/STX_SAMPLE_5MIN.js";
// Reference the charting library.
import { CIQ } from "./js/standard.js";
// Activate the License Key
import getLicenseKey from "./key.js";
getLicenseKey(CIQ);
// My custom marker handler
CIQ.Marker.MyHandler = function (params) {
if (!this.className) this.className = "CIQ.Marker.MyHandler";
CIQ.Marker.call(this, params);
};
// Custom function to add events to the chart
function showEvents() {
var markerTypes = ["dividend", "news", "earnings"],
newNode;
// markers for existing ticks
for (var i = 0; i < stxx.masterData.length; i += 10) {
var r = Math.floor(Math.random() * (markerTypes.length + 1));
if (r == markerTypes.length) continue; // randomize
newNode = document.querySelector("#stxEventPrototype").cloneNode(true);
newNode.id = null;
newNode.innerText = markerTypes[r].charAt(0).toUpperCase();
newNode.classList.add(markerTypes[r]);
new CIQ.Marker.MyHandler({
stx: stxx,
xPositioner: "date",
x: stxx.masterData[i].DT,
label: "events",
node: newNode
});
}
/*
* The code below can be uncommented to show markers for future ticks.
*/
// newNode = document.querySelector("#stxEventPrototype").cloneNode(true);
// newNode.id = null;
// newNode.innerText = "F"; // future tick
// newNode.classList.add("dividend");
// var someDate = new Date(stxx.masterData[stxx.masterData.length - 1].DT);
// someDate = stxx.getNextInterval(someDate, 5); // 5 bars in the future
// new CIQ.Marker.MyHandler({
// stx: stxx,
// xPositioner: "date",
// x: someDate,
// label: "events",
// node: newNode
// });
// stxx.draw();
}
// Instantiate a CIQ.ChartEngine object, the main object for creating charts.
let stxx = new CIQ.ChartEngine({
container: document.querySelector(".chartContainer")
});
// Display the chart. The five-minute periodicity matches the sample data.
stxx.loadChart("SPY", {
masterData: sample5min,
periodicity: {
period: 1,
interval: 5,
timeUnit: "minute"
}
});
// Call the custom showEvents function
showEvents();
</script>
</body>
</html>
Say for example, you want to provide an interface that allows users to add and remove different types of markers individually. Assume you have News events, Dividends events and Earnings events. You can group them together so they can be removed as a group. This is managed by the 'label' parameter within each marker.
To add markers one group at a time, your buttons should send a 'label' identifier to the markers function. This identifier is used to determine the group of events the user wants to display. The code should then display only these events.
To remove those types of events you would use the CIQ.Marker.removeByLabel(stxx, label);
method as stated above.
Your HTML would include something like this:
<div
class="stx-btn stx-menu-btn toggle stx-collapsible"
id="menuEvents"
onclick="toggleEvents('news')"
>
<span>News</span>
</div>
<div
class="stx-btn stx-menu-btn toggle stx-collapsible"
id="menuEvents"
onclick="toggleEvents('earnings')"
>
<span>Earnings</span>
</div>
<div
class="stx-btn stx-menu-btn toggle stx-collapsible"
id="menuEvents"
onclick="toggleEvents('dividend')"
>
<span>Dividends</span>
</div>
Your JS would have something like this:
// object to track the status of each marker group ( displayed or not)
var eventsAreDisplayed = {};
// This function is called by the menu item to turn on or off the particular events
function toggleEvents(typeOfEvent) {
if (eventsAreDisplayed[typeOfEvent]) {
// turn off the flag for these events so you know they are no longer displayed
eventsAreDisplayed[typeOfEvent] = false;
// remove these type of events
CIQ.Marker.removeByLabel(stxx, typeOfEvent);
} else {
showEvents(typeOfEvent);
eventsAreDisplayed[typeOfEvent] = true;
}
}
// This function will get the requested events from the data source and display them on the chart
function showEvents(typeOfEvent) {
if (!stxx.masterData) return;
// your code here to get the events from your source.
var myEvents = ["your list of events for this type from your back office source"];
for (var i = 0; i < myEvents.length; i++) {
// go through your list of markers for this event and add them to the chart
newNode = document.querySelector("#stxEventPrototype").cloneNode(true);
newNode.id = null;
newNode.innerText = "Something to show"; // usually an initial for the marker
newNode.classList.add(typeOfEvent);
new CIQ.Marker.MyHandler({
stx: stxx,
xPositioner: "date",
x: myEvents[i].date, // the date you want the marker to show on
label: typeOfEvent, // the type of event so you can delete them by label letter on
node: newNode
});
}
stxx.draw();
}
Example:
- Click on the
JavaScript
,HTML
andCSS
buttons to see the code for this working sample. - Click on the
Edit in JSFiddle
button to 'play' with this code.
Fixed Markers
Sometimes you may wish to place a marker in a fixed position on the chart. This can be accomplished with the following code:
new CIQ.Marker({
node: yourNode,
xPositioner: "none",
yPositioner: "none"
});
When xPositioner
and yPositioner
are set to "none", the marker will be placed in the panel as an absolutely positioned DOM element. From there, you would use CSS to alter the position.
You can create placement functions that allow you to fix the X position but set a Y position based on value:
new CIQ.Marker.MyHandler({
node: yourNode,
xPositioner: "none"
});
Placement of Markers
Markers by default are placed in the "subholder" for a panel. The subholder is a DIV whose borders are the edges of the panel but not including any-axis. Markers placed in the subholder will "disappear" as the marker slides underneath the axis (left y-axis, right y-axis and/or x-axis).
There are two alternative locations where you can place markers by setting the following fields in your params:
- params.includeAxis=true - If this is true then the marker will be placed in the "holder" which is a DIV that spans the entire space of a panel including the axis. Markers placed with this parameter will overlap axis as you scroll the chart.
- params.chartContainer=true - When this is true the marker is placed in the chart container itself. Placement functions that correctly do the math will still place the marker in the desired spot, but the marker will no longer be bound to the panel and so long as overflow is displayable on the chart container, the marker can overlap the container itself. This is useful for instance with a movable heads up display so that the edges don't get cut off as the user moves their mouse.
Hover and pop-ups
The default Node Creators do not provide hover capabilities. However, as previously stated, markers are simply HTML elements that are moved in sync with the chart as you zoom and pan. Feel free to create these HTML elements with any attributes you wish, including hover capabilities.
Here is a working example of markers with hover functionality:
- Click on the
JavaScript
,HTML
andCSS
buttons to see the code for this working sample. - Click on the
Edit in JSFiddle
button to 'play' with this code.
Grouped Markers
Beginning with version 9.2.0, multiple markers on a single data point will be grouped into one marker instead of being stacked. The grouped marker will display a number indicating the total number of events on that data point, while its color will be determined by the most frequently occurring marker.
To revert to the previous version's behavior, set the groupable
parameter to "false" when creating your markers.
Example:
new CIQ.Marker({
groupable: false,
stx: stxx,
xPositioner: "date",
x: someDate,
node: document.querySelector("yourHTMLElement")
});
Managing Performance
The standard ChartIQ placement functions include code to reduce the amount of DOM manipulation by checking whether a marker is off-screen. If already off-screen then positioning of the marker is skipped. However, if you are using DOM objects as your markers, it can get expensive to move them around the screen, especially if you have a large number of markers.
By default, markers are moved only once per 25 milliseconds. This produces a visual delay effect when panning the chart but dramatically improves chart performance. You can modify this default with the markerDelay variable. Either set it to zero or null to eliminate any delay or set it to the number of milliseconds that your system requires.
Example:
stxx.markerDelay=0;
When thousands of HTML markers need to be placed on the chart, and performance becomes an issue, you may opt to directly render the markers on the canvas itself. You can use CIQ.Marker.Performance to create canvas markers directly. With a performance marker, you apply CSS styles to an HTML <template>
element but never append the template. Instead, use those styles to draw the marker directly on the canvas.
Markers and the quoteFeed
If you wish to link the display of markers to the display of that being loaded via a quoteFeed, you must add additional code to keep them in sync.
There are two approaches to this, depending on where the marker data is coming from.
For more details on creating a quoteFeed see the Quotefeed Tutorial.
Fetching marker data from the main symbol's quoteFeed
In this approach, the data for the markers comes from the same quoteFeed as the data for the main symbol. For example, the following record may be returned:
{
date: 2021-04-04T14:00:00Z,
open: 34.93,
close: 33.98,
high: 35.32,
low: 33.8,
volume: 12300,
...
news: [
{
title: "Investment in Foreign Markets Seems to Pay Off",
url: "https://newssource.com/article/4303284/Inv_Pay_Off.html"
}
]
}
In this case, you can create a function in your quoteFeed implementation and call it when you are formatting the incoming data. Be sure that it is reachable from fetchInitialData
, fetchPaginationData
and fetchUpdateData
methods.
Suppose we call this function addQuoteFeedMarkers
. It might look like this:
function addQuoteFeedMarkers(params, results) {
var stx = params.stx;
// Loop through the data and create markers
results.forEach(function (record) {
var news = record.news;
if (news && news.length) {
var type = "circle";
news.forEach(function (item) {
var marker = new CIQ.Marker({
stx: stx,
label: type,
xPositioner: "date",
x: new Date(record.date),
node: new CIQ.Marker.Simple({
type: type,
category: "news",
headline: item.title,
story: "Click here for article: <a href='" + item.url + "' target='_blank'>" + item.url + "</a>"
})
});
});
};
});
// if you want to display the markers right away, call draw
stx.draw();
}
// called by chart to fetch pagination data
quoteFeedSimulator.fetchPaginationData = function(symbol, startDate, endDate, params, cb) {
var queryUrl =
this.url +
"&identifier=" +
symbol +
"&startdate=" +
startDate.toISOString() +
"&enddate=" +
endDate.toISOString() +
"&interval=" +
params.interval +
"&period=" +
params.period;
CIQ.postAjax(queryUrl, null, function(status, response) {
// process the HTTP response from the datafeed
if (status == 200) {
// if successful response from datafeed
var newQuotes = quoteFeedSimulator.formatChartData(response);
cb({
quotes: newQuotes,
attribution: {
source: "simulator",
exchange: "RANDOM"
}
}); // return fetched data (and set moreAvailable)
/****************** add your markers now *********************/
addQuoteFeedMarkers(params, newQuotes);
/*************************************************************/
} else {
// else error response from datafeed
cb({
error: response ? response : status
}); // specify error in callback
}
});
};
Fetching marker data from a different quoteFeed
(New for 9.0.0)
If the data for the markers comes from a separate quoteFeed, then the approach to loading them is different. In this case, we treat the event similar to how we would treat a series. We call this special series an "event", and the markers it creates, "event markers".
There are three functions that can be used to maintain events:
These functions are analogous to the addSeries
, removeSeries
, and removeAllSeries
functions, with the important difference that the event data is not stored anywhere within the library. In fact, calling addEvent
will create an actual series in stxx.chart.series
.
One important thing to note about the addEvent
function is that is sets the isEvent
property in the series parameters, which will cause the symbolObject of the series to also have the property isEvent=true
. This allows us to set up a new quoteFeed for the event using the isEvent
flag as a filter. Here is an example of attaching an event feed:
stx.attachQuoteFeed(
eventfeed, // engine, you create this quoteFeed.
{ // behavior
refreshInterval: 5,
backgroundRefreshInterval: 60,
bufferSize: 200
},
function (params) { // filter
return params.symbolObject.isEvent;
}
);
Multiple quoteFeeds can be set up by filtering on symbol as well.
An example eventFeed
is provided in the /examples/feeds/ folder of your license package.
Suppose we want to fetch news events. Here's how we would do it:
stxx.addEvent("news", params);
The addEvent
function will call addSeries
, so any parameters supported by addSeries
are supported by addEvent
as well.
By default the addEvent
function will attempt to add Simple markers to the chart after data is fetched.
You can customize this action by passing a processResults
parameter into addEvent
.
When the event is removed, the markers will be removed by default.
To change this behavior, pass a takedownResults
parameter to addEvent
to redefine what happens when the event is removed.
Here is an example of creating and removing event markers:
Assume records take the following form:
{
DT: new Date("2021-04-04T14:00:00Z"),
headline: "Investment in Foreign Markets Seems to Pay Off",
url: "https://newssource.com/article/4303284/Inv_Pay_Off.html"
}
Here are sample processResults
and takedownResults
functions to create and remove markers:
parameters.processResults = function (stx, error, series, records) {
if (error) return;
if (records && records.length) {
var type = "circle";
records.forEach(function (record) {
var item = record.data;
if (item) {
var marker = CIQ.Marker({
stx: stx,
label: type,
xPositioner: "date",
x: record.DT,
node: new CIQ.Marker.Simple({
type: type,
category: "news",
headline: item.headline,
story: "Click here for article: <a href='" + item.url + "' target='_blank'>" + item.url + "</a>"
})
});
}
});
}
stx.draw();
};
parameters.takedownResults = function (stx, seriesId) {
CIQ.Marker.removeByLabel(stx, seriesId);
};
JSFiddle examples
- Markers Secondary series
- Custom hover markers
- Markers in the past
- Trade execution visualization
- Loading markers using ajax
- Adding and removing markers by label