Graphics
Graphics
Custom Indicators now has a Graphics module. We can leverage this in our own indicators to create more complex graphics.
To enable graphics in your indicator, modify the object return value of map
.
//...
map(d) {
return {
graphics: {
items: [
//GraphicsObjects go here.
]
}
}
}
//...
The items
field within graphics tells the renderer what graphics to display. Each item must adhere to the GraphicsObject
interface.
Let's imagine we want to note when a substantial gain has occured at an index on the chart. To do so we must track the last known value. Let's write a tool to help us do this:
function MyTracker(magicNumber) {
function tracker(value) {
return tracker.push(value)
}
tracker.push = (value) => {
let result = value - tracker.state.last >= magicNumber
tracker.state.last = value
return result
}
tracker.reset = () => {
tracker.state = {
last: -1
}
}
tracker.reset()
return tracker
}
On push, the tracker
tool will compare the difference of incoming value and the last known value against a magicNumber
that will be set by
UI parameters. The push
function will return true if the difference is greater than or equal to the magic number, indicating a 'substantial gain',
as defined by the end user. We will use boolean result to determine whether or not to render a graphic at the point of 'substantial gain'.
Now let's build our calculator - the class that will ultimately represent our indicator.
const {px, du, op} = require('./tools/graphics')
class SubstantialGain {
init() {
this.tracker = MyTracker(this.props.magicNumber)
}
map(d) {
const shouldDraw = this.tracker(d.value())
return {
graphics: shouldDraw && {
items: [
{
tag: "Text",
key: "ex",
point: {
x: op(du(d.index()), '-', px(2)),
y: op(du(d.value()), '-', px(58)),
},
text: "!",
style: { fontSize: 18, fontWeight: "bold", fill: "#5c5" },
textAlignment: "centerMiddle"
},
]
}
}
}
module.exports = {
name: "substantialGain",
description: "SubstantialGain",
calculator: SubstantialGain,
params: {
magicNumber: predef.paramSpecs.number(1.00, 0.25, 0.25)
},
tags: ["Drawings"],
}
We first must initialize our tracker tool. We do so in the init
function of our calculator, leveraging our module's props
.
In the map
function, we use our tracker tool to get a boolean value for the bar currently being drawn. Finally in the return
object of our map
, we can add our first graphics item. This particular object is a Text
object. Also, look closely at the point
field of our Text graphics item. We have to declare the point as a ScaleBound value, so we will use the special helper operators op
, du
, and px
. px
allows us to declare a value in the pixel unit-space, while du
utilizes Domain Units. In the X axis, this is the bar index. In the Y axis, domain units are the price of the stock. The op
function is special - it allows us to operate on px
and du
values interchangeably. So we can do things like add absolute pixel values to domain unit values if we want (and we have done exactly that in the example code!). If we were to select our Substantial Gains indicator and supply it with a (reasonable) magic number, we should see some greenish exclamation points
rendered over some of our chart's bars.
That's a great start, but we have substantially more power at our disposal. Let's start by
making our graphic a bit more complex. Within our items
array, let's add another GraphicsObject
.
//...
{
tag: "Shapes",
key: 'circs',
primitives: [
{
tag: 'Circle',
radius: 10,
center: {
x: op(du(d.index()), '-', px(2)),
y: op(du(d.value()), '-', px(60)),
},
},
],
fillStyle: {
color: "#5c5"
}
},
//...
This is another type of GraphicsObject
, the ContourShapes
type. This object describes one or more Shape
objects to apply a border stroke to, as defined by its lineStyle
field. Now we should see a circled exclamation point on bars that have
had 'substantial gains'. Now that we have more than one logically grouped element, we should consider containing them as a unit. To do so,
we can use the Container
type. Replace your items
code with this single GraphicsObject
object.
//...
{
tag: 'Container',
key: 'mediumContainer',
children: [
{
tag: "Text",
key: "ex",
point: {
x: op(du(d.index()), '-', px(2)),
y: op(du(d.value()), '-', px(58)),
},
text: "!",
style: { fontSize: 18, fontWeight: "bold", fill: "#5c5" },
textAlignment: "centerMiddle"
},
{
tag: "ContourShapes",
key: 'circs',
primitives: [
{
tag: 'Circle',
radius: 10,
center: {
x: op(du(d.index()), '-', px(2)),
y: op(du(d.value()), '-', px(60)),
},
},
],
lineStyle: {
lineWidth: 2,
color: "#5c5"
}
},
],
},
//...
The container has one unique field, children
, which is an array of GraphicsObject
s. You can leverage this
grouping to apply transformations that keep these contained objects in relative space. That way you can move the whole
container instead of each object. The other benefit of this is that we can leverage VisibilityConditions
over
whole groups. Here's what we should see when we render this indicator:
To understand our next step, let's do an experiment. First, zoom way out in the X axis - you can do so by scrolling the
mouse wheel. Those circled exclamations don't scale down to fit the new bar size, instead they get clumped up and look bad.
Now zoom way in on the X axis. Now because we haven't scaled up they look somewhat lacking and small. We can fix all of this
by creating a variety of groups to describe each breakpoint and give them a conditions
field. These are the
VisibilityConditions
for the object being rendered.
VisibilityConditions
consist of scalar ranges in the X or Y axis. These are measured in pixels-per-domain-unit. This
works out to roughly the pixel distance between bars in the X axis, and the tick-size in pixels of an index for the given contract
being calculated. What this allows us to do is render something different based on breakpoints we define. Here's an example:
//...
{
tag: 'Container',
key: 'mediumContainer',
conditions: { //<== Added this field!
scaleRangeX: { min: 10 },
},
children: [
//... Same as defined before
],
},
{
tag: 'Container',
key: 'tinyContainer',
conditions: {
scaleRangeX: { min: 0, max: 10 },
},
children: [
{
tag: 'Dots',
key: 'dots',
dots: [
{
point: {
x: du(d.index()),
y: op(du(d.value()), '-', px(48))
},
color: {
r: .25,
g: .8,
b: .25
}
}
],
style: {
lineWidth: 6,
color: '#5c5'
}
},
],
},
//...
Now when the bars are tiny, and have only between 0 and 10 pixels between them, we will render a tiny dot instead of the circled exclamation.
This introduces another GraphicsObject
as well - the Dots
object. It describes one or more WebGL
dots to render. At 10 pixels of space per bar or greater we draw our standard circled exclamation. Note that the scaleRangeX
,
scaleRangeY
, and their min
and max
fields are actually optional. If omitted, we simply won't apply a bound in the ignored
direction or on the ignored axis. We can of course apply something extra for large pixel distances as well.
I'd like to display the amount of the 'substantial gain' at high pixel-per-domain-unit distances. We will need to modify our tracker
slightly to accomodate this - we currently discard our last value on each push. We will could provide the difference in the two values as
part of the return value of our tracker's push
function.
//... in tracker, we simply change push a tiny bit
tracker.push = (value) => {
let number = value - tracker.state.last
let result = number >= magicNumber
tracker.state.last = value
return [result, number]
}
//... in SubstantialGains class we change our tracker's expected return to 2 destructured variables
map(d) {
const [shouldDraw, difference] = this.tracker(d.value());
return {
graphics: shouldDraw && {
items: [
//We only want to add a single extra element - no need for another container
{
conditions: {
scaleRangeX: { min: 30 }
},
tag: "Text",
key: "bigText",
text: `+ ${difference}`,
point: {
x: op(du(d.index()), '-', px(4)),
y: op(du(d.value()), '-', px(84))
},
style: { fontSize: 20, fontWeight: "bold", fill: "#0bf" },
textAlignment: "centerMiddle"
},
//...
]
}
}
}
//...
Rendering our indicator should yield this result at big pixel-to-domain ratios:
Generated using TypeDoc