Get axis value on mouse hover - lightningchart

how do we get X axis value ?
If you see above image , how do we get X axis value if mouse moved over the chart or clicked on anywhere of charts , is there any callback functions ?

You can attach an event handler to chart.onChartBackgroundMouseMove event.
Then you will need to convert the event location to an engine coordinate space. This can be done with publicEngine.engineLocation2Client() method, chart.engine.clientLocation2Engine(ev.clientX, ev.clientY)
The last the is to convert the engine location to a point on a scale. This can be done with translatePoint() function. const onScale = translatePoint(engineLocation, chart.engine.scale, {x: chart.getDefaultAxisX().scale, y: chart.getDefaultAxisY().scale})
chartOHLC.onChartBackgroundMouseMove((obj, ev)=>{
const point = obj.engine.clientLocation2Engine(ev.clientX, ev.clientY)
const onScale = translatePoint(point, obj.engine.scale, {x:obj.getDefaultAxisX().scale, y:obj.getDefaultAxisY().scale})
console.log(onScale)
})
See the snippet below where I log the mouse location on a scale to console.
// Extract required parts from LightningChartJS.
const {
lightningChart,
AxisTickStrategies,
LegendBoxBuilders,
SolidFill,
SolidLine,
emptyLine,
ColorRGBA,
UIOrigins,
Themes,
translatePoint
} = lcjs
// Import data-generator from 'xydata'-library.
const {
createOHLCGenerator,
createProgressiveTraceGenerator
} = xydata
// Create dashboard to house two charts
const db = lightningChart().Dashboard({
// theme: Themes.dark
numberOfRows: 2,
numberOfColumns: 1
})
// Decide on an origin for DateTime axis.
const dateOrigin = new Date(2018, 0, 1)
// Chart that contains the OHLC candle stick series and Bollinger band
const chartOHLC = db.createChartXY({
columnIndex: 0,
rowIndex: 0,
columnSpan: 1,
rowSpan: 1
})
// Use DateTime TickStrategy with custom date origin for X Axis.
chartOHLC
.getDefaultAxisX()
.setTickStrategy(
AxisTickStrategies.DateTime,
(tickStrategy) => tickStrategy.setDateOrigin(dateOrigin)
)
// Modify Chart.
chartOHLC
.setTitle('Trading dashboard')
//Style AutoCursor.
.setAutoCursor(cursor => {
cursor.disposeTickMarkerY()
cursor.setGridStrokeYStyle(emptyLine)
})
.setPadding({
right: 40
})
chartOHLC.onChartBackgroundMouseMove((obj, ev) => {
const point = obj.engine.clientLocation2Engine(ev.clientX, ev.clientY)
const onScale = translatePoint(point, obj.engine.scale, {
x: obj.getDefaultAxisX().scale,
y: obj.getDefaultAxisY().scale
})
console.log(onScale)
})
// The top chart should have 66% of view height allocated to it. By giving the first row a height of 2, the relative
// height of the row becomes 2/3 of the whole view (default value for row height / column width is 1)
db.setRowHeight(0, 2)
// Create a LegendBox for Candle-Stick and Bollinger Band
const legendBoxOHLC = chartOHLC.addLegendBox(LegendBoxBuilders.HorizontalLegendBox)
.setPosition({
x: 100,
y: 100
})
.setOrigin(UIOrigins.RightTop)
// Define function which sets Y axis intervals nicely.
let setViewNicely
// Create OHLC Figures and Area-range.
//#region
// Get Y-axis for series (view is set manually).
const stockAxisY = chartOHLC.getDefaultAxisY()
.setScrollStrategy(undefined)
.setTitle('USD')
// Add series.
const areaRangeFill = new SolidFill().setColor(ColorRGBA(100, 149, 237, 50))
const areaRangeStroke = new SolidLine()
.setFillStyle(new SolidFill().setColor(ColorRGBA(100, 149, 237)))
.setThickness(1)
const areaRange = chartOHLC.addAreaRangeSeries({
yAxis: stockAxisY
})
.setName('Bollinger band')
.setHighFillStyle(areaRangeFill)
.setLowFillStyle(areaRangeFill)
.setHighStrokeStyle(areaRangeStroke)
.setLowStrokeStyle(areaRangeStroke)
.setMouseInteractions(false)
.setCursorEnabled(false)
const stockFigureWidth = 5.0
const borderBlack = new SolidLine().setFillStyle(new SolidFill().setColor(ColorRGBA(0, 0, 0))).setThickness(1.0)
const fillBrightRed = new SolidFill().setColor(ColorRGBA(255, 0, 0))
const fillDimRed = new SolidFill().setColor(ColorRGBA(128, 0, 0))
const fillBrightGreen = new SolidFill().setColor(ColorRGBA(0, 255, 0))
const fillDimGreen = new SolidFill().setColor(ColorRGBA(0, 128, 0))
const stock = chartOHLC.addOHLCSeries({
yAxis: stockAxisY
})
.setName('Candle-Sticks')
// Setting width of figures
.setFigureWidth(stockFigureWidth)
// Styling positive candlestick
.setPositiveStyle(candlestick => candlestick
// Candlestick body fill style
.setBodyFillStyle(fillBrightGreen)
// Candlestick body fill style
.setBodyStrokeStyle(borderBlack)
// Candlestick stroke style
.setStrokeStyle((strokeStyle) => strokeStyle.setFillStyle(fillDimGreen))
)
.setNegativeStyle(candlestick => candlestick
.setBodyFillStyle(fillBrightRed)
.setBodyStrokeStyle(borderBlack)
.setStrokeStyle((strokeStyle) => strokeStyle.setFillStyle(fillDimRed))
)
// Make function that handles adding OHLC segments to series.
const add = (ohlc) => {
// ohlc is equal to [x, open, high, low, close]
stock.add(ohlc)
// AreaRange looks better if it extends a bit further than the actual OHLC values.
const areaOffset = 0.2
areaRange.add({
position: ohlc[0],
high: ohlc[2] - areaOffset,
low: ohlc[3] + areaOffset,
})
}
// Generate some static data.
createOHLCGenerator()
.setNumberOfPoints(100)
.setDataFrequency(24 * 60 * 60 * 1000)
.generate()
.toPromise()
.then(data => {
data.forEach(add)
setViewNicely()
})
//#endregion
// Create volume.
//#region
const chartVolume = db.createChartXY({
columnIndex: 0,
rowIndex: 1,
columnSpan: 1,
rowSpan: 1
})
// Use DateTime TickStrategy with custom date origin for X Axis.
chartVolume
.getDefaultAxisX()
.setTickStrategy(
AxisTickStrategies.DateTime,
(tickStrategy) => tickStrategy.setDateOrigin(dateOrigin)
)
// Modify Chart.
chartVolume
.setTitle('Volume')
.setPadding({
right: 40
})
// Create a LegendBox as part of the chart.
const legendBoxVolume = chartVolume.addLegendBox(LegendBoxBuilders.HorizontalLegendBox)
.setPosition({
x: 100,
y: 100
})
.setOrigin(UIOrigins.RightTop)
// Create Y-axis for series (view is set manually).
const volumeAxisY = chartVolume.getDefaultAxisY()
.setTitle('USD')
// Modify TickStyle to hide gridstrokes.
.setTickStrategy(
// Base TickStrategy that should be styled.
AxisTickStrategies.Numeric,
// Modify the the tickStyles through a mutator.
(tickStrat) => tickStrat
// Modify the Major tickStyle to not render the grid strokes.
.setMajorTickStyle(
tickStyle => tickStyle.setGridStrokeStyle(emptyLine)
)
// Modify the Minor tickStyle to not render the grid strokes.
.setMinorTickStyle(
tickStyle => tickStyle.setGridStrokeStyle(emptyLine)
)
)
const volumeFillStyle = new SolidFill().setColor(ColorRGBA(0, 128, 128, 60))
const volumeStrokeStyle = new SolidLine()
.setFillStyle(volumeFillStyle.setA(255))
.setThickness(1)
const volume = chartVolume.addAreaSeries({
yAxis: volumeAxisY
})
.setName('Volume')
.setFillStyle(volumeFillStyle)
.setStrokeStyle(volumeStrokeStyle)
createProgressiveTraceGenerator()
.setNumberOfPoints(990)
.generate()
.toPromise()
.then(data => {
volume.add(data.map(point => ({
x: point.x * 2.4 * 60 * 60 * 1000,
y: Math.abs(point.y) * 10
})))
setViewNicely()
})
//#endregion
// Add series to LegendBox and style entries.
const entries1 = legendBoxOHLC.add(chartOHLC)
entries1[0]
.setButtonOnFillStyle(areaRangeStroke.getFillStyle())
.setButtonOnStrokeStyle(emptyLine)
const entries2 = legendBoxVolume.add(chartVolume)
entries2[0]
.setButtonOnFillStyle(volumeStrokeStyle.getFillStyle())
.setButtonOnStrokeStyle(emptyLine)
setViewNicely = () => {
const yBoundsStock = {
min: areaRange.getYMin(),
max: areaRange.getYMax(),
range: areaRange.getYMax() - areaRange.getYMin()
}
const yBoundsVolume = {
min: volume.getYMin(),
max: volume.getYMax(),
range: volume.getYMax() - volume.getYMin()
}
// Set Y axis intervals so that series don't overlap and volume is under stocks.
volumeAxisY.setInterval(yBoundsVolume.min, yBoundsVolume.max)
stockAxisY.setInterval(yBoundsStock.min - yBoundsStock.range * .33, yBoundsStock.max)
}
stock.setResultTableFormatter((builder, series, segment) => {
return builder
.addRow(series.getName())
.addRow(series.axisX.formatValue(segment.getPosition()))
.addRow('Open ' + segment.getOpen().toFixed(2))
.addRow('High ' + segment.getHigh().toFixed(2))
.addRow('Low ' + segment.getLow().toFixed(2))
.addRow('Close ' + segment.getClose().toFixed(2))
})
volume.setResultTableFormatter((builder, series, position, high, low) => {
return builder
.addRow(series.getName())
.addRow(series.axisX.formatValue(position))
.addRow('Value ' + Math.round(high))
.addRow('Base ' + Math.round(low))
})
<script src="https://unpkg.com/#arction/xydata#1.4.0/dist/xydata.iife.js"></script>
<script src="https://unpkg.com/#arction/lcjs#2.2.1/dist/lcjs.iife.js"></script>

Related

Increase size of one of the series in R highchart

I'm trying to show a line and % changes in a single highchart plot, but the changes are very little and It can't be seen in the plot. I made a simplified code to show my problem:
a <- c(300,200, 400, 10, 40, 80)
b <- c(0.8, 2, -2, -1.5, -1.1, 2)
d<-cbind(a,b)
dt <- seq(as.Date("2018-01-01"), as.Date("2018-01-06"), by = "days")
ts <- xts(d, dt )
highchart(type="stock") %>%
hc_add_series(ts$a,
type = "line",
color="black") %>%
hc_add_series(ts$b,
type = "lollipop",
color="red")
I need to increase the size of "ts$b" in the plot, how can I do it? I also tried with two axis, but It seems doesn't solve the problem.
I see two solutions to achieve that.
The first you mentioned - using two yAxis and manipulating their height and top distance.
Example JS code:
yAxis: [{
height: '90%',
opposite: false
},
{
visible: false,
top: '83%',
height: '15%',
}
]
Demo:
https://jsfiddle.net/BlackLabel/0826r7sh/
Another way is using a modified logarithmic axis. Negative values can't be plotted on a log axis, because by nature, the axis will only show positive values. In that case you need to use a custom extension according to the following thread:
Highcharts negative logarithmic scale solution stopped working
(function(H) {
H.addEvent(H.Axis, 'afterInit', function() {
const logarithmic = this.logarithmic;
if (logarithmic && this.options.custom.allowNegativeLog) {
// Avoid errors on negative numbers on a log axis
this.positiveValuesOnly = false;
// Override the converter functions
logarithmic.log2lin = num => {
const isNegative = num < 0;
let adjustedNum = Math.abs(num);
if (adjustedNum < 10) {
adjustedNum += (10 - adjustedNum) / 10;
}
const result = Math.log(adjustedNum) / Math.LN10;
return isNegative ? -result : result;
};
logarithmic.lin2log = num => {
const isNegative = num < 0;
let result = Math.pow(10, Math.abs(num));
if (result < 10) {
result = (10 * (result - 1)) / (10 - 1);
}
return isNegative ? -result : result;
};
}
});
}(Highcharts));
.
yAxis: {
type: 'logarithmic',
custom: {
allowNegativeLog: true
}
},
Demo
https://jsfiddle.net/BlackLabel/nw6osucm/

CSS Scaling of img in React changes position offset behavior

I am building a component that will display any image inside a parent div and allows dragging of the image (when larger than the div) as well as scaling on both a double click or pinch on mobile. I am using inline style changes on the image to test the behavior and so far everything works as I wanted...except that when I change the image transform:scale() all the calculations that effectively set the correct limits to prevent offsetting an image outside the parent div no longer behave as expected. It does work perfectly if I keep the scale=1.
So for example, with a parent Div width and height of 500/500 respectively, and an image that say is 1500x1000, I prevent any "over"offsetting when dragging the image by setting limits of leftLimit = -(1500-500) = -1000 and topLimit = -(1000-500) = -500. This works perfectly at the initial scale of 1. However, I have a dblClick event that scales the image upwards at .5 intervals, and once the scale changes from 1 to any other number, the methods I use above to calculate offset Limits are no longer value. So for example if I increase the scale to 2, in theory that same 1500x1000 image in a 500x500 div should NOW have leftLimit = -(3000-500) = -2500 and topLimit = -(2000-500) = 1500. But these new calculated limits allow the image to be dragged right out of the parent div region. For reference here is the code. Any help or methods for testing what's actually going on would be very much appreciated.
Note the image is being loaded as a file for test, it's a fairly large base64 string. The code is as follows (btw, I am figuring my use of so many 'state' variables probably exposes my ignorance of how such values could/should really persist across renderings. I am still quite new to React) :
import * as React from 'react'
import * as types from '../../types/rpg-types'
import imgSource from './testwebmap.jpg'
import * as vars from '../../data/mapImage'
const testImg = require('./testwebmap.jpg')
export default function MyMapImage() {
let divRef = React.useRef<HTMLDivElement>(null)
let imgRef = React.useRef<HTMLImageElement>(null)
const [loaded, setLoaded] = React.useState<boolean>(false)
const [imgTop, setImgTop] = React.useState<number>(0)
const [imgLeft, setImgLeft] = React.useState<number>(0)
const [scHeight, setSCHeight] = React.useState<number>(100)
const [scWidth, setSCWidth] = React.useState<number>(100)
const [imgScale, setImgScale] = React.useState<number>(1)
const [natHeight, setNatHeight] = React.useState<number>(100)
const [natWidth, setNatWidth] = React.useState<number>(100)
const [oldXCoord, setOldXCoord] = React.useState<number>(0)
const [oldYCoord, setOldYCoord] = React.useState<number>(0)
const [topLimit, setTopLimit] = React.useState<number>(0)
const [leftLimit, setLeftLimit] = React.useState<number>(0)
const [isScaling, setIsScaling] = React.useState<boolean>(false)
const [isDragging, setIsDragging] = React.useState<boolean>(false)
const [isFirstPress, setIsFirstPress] = React.useState<boolean>(false)
const [accel, setAccel] = React.useState<number>(1)
const [touchDist, setTouchDist] = React.useState<number>(0)
const [cfg, setCfg] = React.useState<types.ImageConfig>({
img: '',
imgTOP: 0,
imgLEFT: 0,
offsetX: 0,
offsetY: 0,
isFirstPress: true,
isDragging: false,
isScaling: false,
divHeight: 500,
divWidth: 500,
topLimit: 0,
leftLimit: 0,
isLoaded: true,
oldMouseX: 0,
oldMouseY: 0,
touchDist: 0,
})
const setNewImageLimits = () => {
const img = imgRef
let heightLimit: number
let widthLimit: number
console.log(`imgScale is: ${imgScale}`)
//console.log(`current offsets: ${imgLeft}:${imgTop}`)
console.log(`img width/Height: ${img.current?.width}:${img.current?.height}`)
console.log(img)
img.current
? (heightLimit = Math.floor(imgScale * img.current.naturalHeight - cfg.divHeight))
: (heightLimit = 0)
img.current
? (widthLimit = Math.floor(imgScale * img.current.naturalWidth - cfg.divWidth))
: (widthLimit = 0)
setTopLimit(-heightLimit)
setLeftLimit(-widthLimit)
setImgLeft(0)
setImgTop(0)
console.log(
'New Image limits set with topLimit:' + heightLimit + ' and leftLimit:' + widthLimit
)
}
const handleImageLoad = () => {
if (imgRef) {
const img = imgRef
//console.log(imgRef)
let heightLimit: number
let widthLimit: number
img.current ? (heightLimit = img.current.naturalHeight - cfg.divHeight) : (heightLimit = 0)
img.current ? (widthLimit = img.current.naturalWidth - cfg.divWidth) : (widthLimit = 0)
setTopLimit(-heightLimit)
setLeftLimit(-widthLimit)
setNatHeight(img.current ? img.current.naturalHeight : 0)
setNatWidth(img.current ? img.current.naturalWidth : 0)
setSCHeight(img.current ? img.current.naturalHeight : 0)
setSCWidth(img.current ? img.current.naturalWidth : 0)
console.log('Image Loaded with topLimit:' + heightLimit + ' and leftLimit:' + widthLimit)
}
}
React.useEffect(() => {
if (imgRef.current?.complete) {
handleImageLoad()
}
}, [])
React.useEffect(() => {
setNewImageLimits()
console.log(`imgScale is: ${imgScale}`)
console.log('Image has with topLimit:' + topLimit + ' and leftLimit:' + leftLimit)
}, [imgScale])
function distance(e: any) {
let zw = e.touches[0].pageX - e.touches[1].pageX
let zh = e.touches[0].pageY - e.touches[1].pageY
if (zw * zw + zh * zh != 0) {
return Math.sqrt(zw * zw + zh * zh)
} else return 0
}
function setCoordinates(e: any) {
let canMouseX: number
let canMouseY: number
if (e?.nativeEvent?.clientX && e?.nativeEvent?.clientY) {
//console.log(e)
//canMouseX = parseInt(e.clientX - cfg.offsetX)
canMouseX = e.nativeEvent.clientX - cfg.offsetX
canMouseY = e.nativeEvent.clientY - cfg.offsetY
//console.log(`${canMouseX}:${canMouseY}`)
} else if (e?.nativeEvent?.targetTouches) {
canMouseX = e.nativeEvent.targetTouches.item(0)?.clientX - cfg.offsetX
canMouseY = e.nativeEvent.targetTouches.item(0)?.clientY - cfg.offsetY
// This isn't doing anything (noticeable)
// e.preventDefault();
} else return {}
return {
canMouseX,
canMouseY,
}
}
const handleMouseUp = (e: any) => {
let { canMouseX, canMouseY } = setCoordinates(e)
setIsScaling(false)
setIsDragging(false)
setIsFirstPress(true)
setAccel(1)
console.log('Mouse UP Event function')
}
const handleMouseDown = (e: any) => {
const { canMouseX, canMouseY } = setCoordinates(e)
//console.log('Mouse DOWN Event function')
e.preventDefault()
//console.log(`Mouse Down ${canMouseX}:${canMouseY}`)
canMouseX ? setOldXCoord(canMouseX) : setOldXCoord(0)
canMouseY ? setOldYCoord(canMouseY) : setOldYCoord(0)
setIsDragging(true)
setCfg({ ...cfg, isDragging: true })
if (e?.targetTouches) {
e.preventDefault()
if (e?.nativeEvent?.touches?.length > 1) {
// detected a pinch
setTouchDist(distance(e))
setCfg({ ...cfg, touchDist: distance(e), isScaling: true })
setIsScaling(true)
setIsDragging(false)
} else {
// set the drag flag
setIsScaling(false)
setIsDragging(true)
}
}
setIsFirstPress(false)
setCfg({ ...cfg, isFirstPress: true })
}
const handleDoubleClick = (e: any) => {
const { canMouseX, canMouseY } = setCoordinates(e)
if (imgScale === 3) {
setImgScale(1)
} else {
setImgScale(imgScale + 0.5)
}
}
const handleMouseMove = (e: any) => {
let scaling = isScaling
let dragging = isDragging
let tempImgScale: number = 1
const { canMouseX, canMouseY } = setCoordinates(e)
let yDiff: number
let xDiff: number
let newLeft: number
let newTop: number
if (e.targetTouches) {
e.preventDefault()
if (e.touches.length > 1) {
//detected a pinch
setIsScaling(true)
setIsDragging(false)
scaling = true
} else {
setIsScaling(false)
setIsDragging(true)
}
}
//console.log(`isScaling : ${isScaling}`)
if (scaling) {
//...adding rndScaleTest to force processing of scaling randomly
let dist = distance(e)
//Can't divide by zero, so return dist in denom. if touchDist still at initial 0 value
tempImgScale = dist / (touchDist === 0 ? dist : touchDist)
//console.log(`imgScale is: ${imgScale}`)
if (tempImgScale < 1) tempImgScale = 1 //for now no scaling down allowed...
if (tempImgScale > 2) tempImgScale = 2 //...and scaling up limited to 2.5x
setSCHeight(Math.floor(imgScale * natHeight))
setSCWidth(Math.floor(imgScale * natWidth))
setImgScale(tempImgScale)
setTouchDist(dist)
}
// if the drag flag is set, clear the canvas and draw the image
if (isDragging) {
yDiff = canMouseY && oldYCoord ? accel * (canMouseY - oldYCoord) : 0
xDiff = canMouseX && oldXCoord ? accel * (canMouseX - oldXCoord) : 0
if (imgLeft + xDiff <= leftLimit) {
setImgLeft(leftLimit)
} else if (imgLeft + xDiff >= 0) {
setImgLeft(0)
} else setImgLeft(imgLeft + xDiff)
if (imgTop + yDiff <= topLimit) {
setImgTop(topLimit)
} else if (imgTop + yDiff >= 0) {
setImgTop(0)
} else setImgTop(imgTop + yDiff)
if (accel < 4) {
setAccel(accel + 1)
}
}
//console.log('Mouse **MOVE Event function')
setOldXCoord(canMouseX || 0)
setOldYCoord(canMouseY || 0)
}
const handleMouseLeave = (e: any) => {
setIsScaling(false)
setIsDragging(false)
setIsFirstPress(true)
setAccel(1)
console.log('Mouse LEAVE Event function')
}
return (
<div>
<div className="portrait">
<div
ref={divRef}
className="wrapper"
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onTouchEnd={handleMouseUp}
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
onTouchMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onDoubleClick={handleDoubleClick}
>
<img
ref={imgRef}
src={`data:image/jpeg;base64,${vars.bigImage}`}
style={{
transform: `scale(${imgScale})`,
transformOrigin: `top left`,
objectPosition: `${imgLeft}px ${imgTop}px`,
}}
onLoad={handleImageLoad}
/>
</div>
</div>
<span>{`imgLeft: ${imgLeft}px `}</span>
<span>{`imgTop: ${imgTop}px `}</span>
</div>
)
}
In the end, the problem does lie with the use of Scale(), and I could not rely on it for purposes of tracking position and offset. So I ended up explicitly scaling 'height' and 'width' for the img and keeping the scaling value as a reference when required to calculate new values for these. The parent Div and Img essentially now look like this:
<div
ref={divRef}
className="wrapper"
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onTouchEnd={handleMouseUp}
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
onTouchMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onDoubleClick={handleDoubleClick}
>
<img
ref={imgRef}
src={`data:image/jpeg;base64,${vars.bigImage}`}
style={{
transform: `translate(${imgLeft}px, ${imgTop}px)`,
height: `${scHeight}px`,
width: `${scWidth}px)`,
transformOrigin: `top left`,
}}
onLoad={handleImageLoad}
/>
</div>
This works fine, calculated offsets match the expected values, and translation of the image within the parent div behaves as expected.

heatmapGridSeries overlay over line chart

I came Across heatmapGridSeries in lightning chart , I just want to know if this is possible.
Please Check above image , I have lineseries with one axis.. and i want this color bands above the line series with opacity.
For example
if I add value to the heatmapGridSeries from 0 to 100 , it should automatically show orange.. from 100 to 200 it should show green and so on.
Can be done, but shouldn't be a very practical usage case for heatmaps.
Paletted/gradient series background or bands should give nicer results.
Here's a snippet that looks like your picture (without the line series), and also adds a single Band so you can see how they behave a bit differently.
Transparency can be added after R,G,B.
const {
lightningChart,
LUT,
PalettedFill,
ColorRGBA,
emptyLine,
SolidFill,
AxisScrollStrategies,
} = lcjs
const chart = lightningChart().ChartXY()
const line = chart.addLineSeries()
const heatmap = chart.addHeatmapScrollingGridSeries({
scrollDimension: 'columns',
resolution: 1,
step: {
x: 1,
y: 1,
},
})
const transparency = 100
heatmap
.setFillStyle(new PalettedFill({
lut: new LUT({
steps: [
{value: 0, color: ColorRGBA(255,127,39, transparency)},
{value: 1, color: ColorRGBA(181,230,29, transparency)},
{value: 2, color: ColorRGBA(112,146,190, transparency)},
{value: 3, color: ColorRGBA(255,242,0, transparency)},
{value: 4, color: ColorRGBA(237,28,36, transparency)}
]
})
}))
.setPixelInterpolationMode('disabled')
.setWireframeStyle(emptyLine)
chart.getDefaultAxisX().setInterval(-10, 0).setScrollStrategy(AxisScrollStrategies.progressive)
line.add({x:0,y:0})
let x = 1
setInterval(() => {
const y = Math.random()
const p = {x, y}
line.add(p)
const iColor = x % 5
heatmap.addIntensityValues([[iColor]])
x += 1
}, 1000)
<script src="http://unpkg.com/#arction/lcjs#3.1.0/dist/lcjs.iife.js"></script>

Select points in line series

I'd like to use a modifier key with the left mouse button that will select the data inside the rectangle, rather than the zoom to that data. Is this possible? I cannot find a suitable API for it. Bonus points if there's a way to select data that falls inside a polygon (like a lasso tool).
Here's one example of completely custom ChartXY interactions.
Key points:
Default rectangle fit & zoom interactions are disabled.
Line series data is cached to a variable which can be used for custom statistics.
RectangleSeries is used for visualizing drag area on chart.
UI elements are used for displaying statistics of selected area.
ChartXY.onSeriesBackgroundMouseDrag event is used for hooking custom actions to user interactions.
Below you'll find a code snippet where dragging with left mouse button creates a rectangular area which shows highlighted X area and solved Y data range within.
Releasing the mouse button results in the full selected data points array being solved (length is logged to console).
const {
Point,
ColorRGBA,
SolidFill,
RadialGradientFill,
SolidLine,
translatePoint,
lightningChart,
UIOrigins,
UIElementBuilders,
UILayoutBuilders,
emptyFill
} = lcjs;
const { createProgressiveTraceGenerator } = xydata;
const chart = lightningChart()
.ChartXY()
// Disable default chart interactions with left mouse button.
.setMouseInteractionRectangleFit(false)
.setMouseInteractionRectangleZoom(false)
.setTitleFillStyle(emptyFill)
const axisX = chart.getDefaultAxisX()
const axisY = chart.getDefaultAxisY()
const lineSeries = chart.addLineSeries({
dataPattern: {
pattern: 'ProgressiveX',
},
})
// Generate test data set.
let dataSet
createProgressiveTraceGenerator()
.setNumberOfPoints(10 * 1000)
.generate()
.toPromise()
.then((data) => {
// Cache data set for analytics logic + add static data to series.
dataSet = data
lineSeries.add(data)
})
// Rectangle Series is used to display data selection area.
const rectangleSeries = chart.addRectangleSeries()
const rectangle = rectangleSeries
.add({ x1: 0, y1: 0, x2: 0, y2: 0 })
.setFillStyle(
new RadialGradientFill({
stops: [
{ offset: 0, color: ColorRGBA(255, 255, 255, 30) },
{ offset: 1, color: ColorRGBA(255, 255, 255, 60) },
],
}),
)
.setStrokeStyle(
new SolidLine({
thickness: 2,
fillStyle: new SolidFill({ color: ColorRGBA(255, 255, 255, 255) }),
}),
)
.dispose()
// UI elements are used to display information about the selected data points.
const uiInformationLayout = chart.addUIElement(UILayoutBuilders.Column, { x: axisX, y: axisY }).dispose()
const uiLabel0 = uiInformationLayout.addElement(UIElementBuilders.TextBox)
const uiLabel1 = uiInformationLayout.addElement(UIElementBuilders.TextBox)
// Add events for custom interactions.
chart.onSeriesBackgroundMouseDrag((_, event, button, startLocation) => {
// If not left mouse button, don't do anything.
if (button !== 0) return
// Translate start location and current location to axis coordinates.
const startLocationAxis = translatePoint(
chart.engine.clientLocation2Engine(startLocation.x, startLocation.y),
chart.engine.scale,
lineSeries.scale,
)
const curLocationAxis = translatePoint(
chart.engine.clientLocation2Engine(event.clientX, event.clientY),
chart.engine.scale,
lineSeries.scale,
)
// Place Rectangle figure between start location and current location.
rectangle.restore().setDimensions({
x1: startLocationAxis.x,
y1: startLocationAxis.y,
x2: curLocationAxis.x,
y2: curLocationAxis.y,
})
// * Gather analytics from actively selected data *
const xStart = Math.min(startLocationAxis.x, curLocationAxis.x)
const xEnd = Math.max(startLocationAxis.x, curLocationAxis.x)
// Selected Y range has to be solved from data set.
// NOTE: For top solve performance, results should be cached and only changes from previous selection area should be checked.
const { yMin, yMax } = solveDataRangeY(xStart, xEnd)
// Set UI labels text.
uiLabel0.setText(`X: [${xStart.toFixed(0)}, ${xEnd.toFixed(0)}]`)
uiLabel1.setText(`Y: [${yMin.toFixed(1)}, ${yMax.toFixed(1)}]`)
// Place UI layout above Rectangle.
uiInformationLayout
.restore()
.setOrigin(UIOrigins.LeftBottom)
.setPosition({ x: xStart, y: Math.max(startLocationAxis.y, curLocationAxis.y) })
})
chart.onSeriesBackgroundMouseDragStop((_, event, button, startLocation) => {
// If not left mouse button, don't do anything.
if (button !== 0) return
// Translate start location and current location to axis coordinates.
const startLocationAxis = translatePoint(
chart.engine.clientLocation2Engine(startLocation.x, startLocation.y),
chart.engine.scale,
lineSeries.scale,
)
const curLocationAxis = translatePoint(
chart.engine.clientLocation2Engine(event.clientX, event.clientY),
chart.engine.scale,
lineSeries.scale,
)
// Print selected data points to console.
const xStart = Math.max(0, Math.floor(Math.min(startLocationAxis.x, curLocationAxis.x)))
const xEnd = Math.min(dataSet.length - 1, Math.ceil(Math.max(startLocationAxis.x, curLocationAxis.x)))
const selectedDataPoints = dataSet.slice(xStart, xEnd)
console.log(`Selected ${selectedDataPoints.length} data points.`)
// Hide visuals.
rectangle.dispose()
uiInformationLayout.dispose()
})
// Logic for solving Y data range between supplied X range from active data set.
const solveDataRangeY = (xStart, xEnd) => {
// Reduce Y data min and max values within specified X range from data set.
// Note, this can be very heavy for large data sets - repeative calls should be avoided as much as possible for best performance.
let yMin = Number.MAX_SAFE_INTEGER
let yMax = -Number.MAX_SAFE_INTEGER
xStart = Math.max(0, Math.floor(xStart))
xEnd = Math.min(dataSet.length - 1, Math.ceil(xEnd))
for (let iX = xStart; iX < xEnd; iX += 1) {
const y = dataSet[iX].y
yMin = y < yMin ? y : yMin
yMax = y > yMax ? y : yMax
}
return { yMin, yMax }
}
<script src="https://unpkg.com/#arction/xydata#1.4.0/dist/xydata.iife.js"></script>
<script src="https://unpkg.com/#arction/lcjs#3.0.0/dist/lcjs.iife.js"></script>
There's many different directions to go with this kind of custom interactions, and while we can't cover every single one with an example, most of the logic should stay the same.

Draw lines from one X-axis to another X-axis

I want to draw line from one x-axis to another x-axis , is it possible ?
Right now I can see only constant line is there , but its drawing horizontal line in full page.
const axis = chart.getDefaultAxisX();
const line = axis.addConstantLine(false).setMouseInteractions(false);
Is there any other way to do it like with rectangle ? or do you guys have any plan to do that via constant line in future ?
I have attached example image below
If I have understood your question correctly you want to draw a line from one X axis value to another X axis value on the same axis.
This can be done by using ChartXY.addSegmentSeries(). This series type allows you to draw individual line segments from Point A to Point B. Each segment can be configured individually. See the API Documentation for configuration specifics: https://www.arction.com/lightningchart-js-api-documentation/v3.0.0/classes/segmentfigure.html
To achieve a line from X = 30 to X = 70 you need to first create a new SegmentSeries if you don't already have one.
You can specify the X (or/and Y) axis for the SegmentSeries when creating the series. chart.addSegmentSeries({xAxis: axisX1})
const axisX1 = chart.addAxisX()
// create series
const lineSegment = chart.addSegmentSeries({
xAxis: axisX1
})
// optionally disable cursor and mouse interactions
.setCursorEnabled(false)
.setMouseInteractions(false)
Then you can create a new line on the series
lineSegment.add({
startX: 30,
endX: 70,
startY: 0.2,
endY: 0.2
})
.setStrokeStyle(stroke => stroke.setFillStyle(fill => fill.setColor(ColorHEX('#0f0a'))))
This would create semi-transparent green line from {x: 30, y: 0.2} to {x: 70, y: 0.2}.
You can create as many lines as you need just by calling lineSegment.add() again with new startX/Y and endX/Y values.
Alternatively if you would like to have a rectangle on the chart instead of line you can add a new RectangleSeries. And add a new rectangle on that series.
const axisX2 = chart.addAxisX()
const rectSeries = chart.addRectangleSeries({
xAxis: axisX2
})
.setMouseInteractions(false)
.setCursorEnabled(false)
rectSeries.add({
x1: 30,
x2: 70,
y1: 0.3,
y2: 0.7,
})
.setFillStyle(f => f.setColor(ColorHEX('#f00a')))
See below for a working example with both line and rectangle.
// Extract required parts from LightningChartJS.
const {
lightningChart,
Themes,
UIElementBuilders,
UIBackgrounds,
ColorHEX,
SolidFill
} = lcjs
// Import data-generator from 'xydata'-library.
const {
createProgressiveRandomGenerator
} = xydata
const chart = lightningChart().ChartXY()
const axisX1 = chart.getDefaultAxisX()
const axisX2 = chart.addAxisX()
const axisX3 = chart.addAxisX({
opposite: true
})
const series = chart.addLineSeries()
chart.getDefaultAxisX().setInterval(0, 100, false, true)
chart.getDefaultAxisY().setInterval(0, 1, false, true)
createProgressiveRandomGenerator()
.setNumberOfPoints(100)
.generate()
.toPromise()
.then(data => {
series.add(data)
})
const rectSeries = chart.addRectangleSeries({
xAxis: axisX2
})
.setMouseInteractions(false)
.setCursorEnabled(false)
rectSeries.add({
x1: 30,
x2: 70,
y1: 0.3,
y2: 0.7,
})
.setFillStyle(f => f.setColor(ColorHEX('#f00a')))
const lineSegment = chart.addSegmentSeries({
xAxis: axisX3
})
.setCursorEnabled(false)
.setMouseInteractions(false)
lineSegment.add({
startX: 30,
endX: 70,
startY: 0.2,
endY: 0.2
})
.setStrokeStyle(stroke => stroke.setFillStyle(fill => fill.setColor(ColorHEX('#0f0a'))))
lineSegment.add({
startX: 40,
endX: 90,
startY: 0.2,
endY: 0.1
})
.setStrokeStyle(stroke => stroke.setFillStyle(fill => fill.setColor(ColorHEX('#fffa'))))
<script src="https://unpkg.com/#arction/lcjs#3.0.0/dist/lcjs.iife.js"></script>
<script src="https://unpkg.com/#arction/xydata#1.4.0/dist/xydata.iife.js"></script>
You can use the Axis Band to draw a rectangle which will work similar to the constant line.
const axis = chart.getDefaultAxisX()
const band = axis.addBand()
You can set both the constant line and band to be either on top of all the Series in the chart, or below all Series in the chart, by supplying the onTop: boolean parameter when creating one.
You can set the start and end values of the Band with the Band.setValueStart() and Band.setValueEnd() methods respectively.
For example:
band.setValueStart(100)
band.setValueEnd(200)
This would set the Band cover the range from 100 to 200.
If you have mouse interactions enabled for the band (which is on by default), users can also click and drag the band from its edges to resize it.
The full API documentation for band can be found here.

Resources