import {
AnnotationDragDeltaEventArgs,
CustomAnnotation,
DefaultPaletteProvider,
EAutoRange,
EAxisAlignment,
ECoordinateMode,
EHorizontalAnchorPoint,
ELegendOrientation,
EStrokePaletteMode,
EVerticalAnchorPoint,
EXyDirection,
FastLineRenderableSeries,
FastMountainRenderableSeries,
GradientParams,
IRenderableSeries,
LegendModifier,
libraryVersion,
MouseWheelZoomModifier,
NumberRange,
NumericAxis,
Point,
SciChartJsNavyTheme,
SciChartSurface,
SeriesSelectionModifier,
Thickness,
TSciChart,
XyDataSeries,
ZoomExtentsModifier,
ZoomPanModifier,
} from "scichart";
import { Radix2FFT } from "./Radix2FFT";
export const divMainChartId = "sciChart1";
export const divCrossSection1 = "sciChart2";
export const divCrossSection2 = "sciChart3";
// This function generates some spectral data for the waterfall chart
const createSpectralData = (n: number) => {
const spectraSize = 1024;
const timeData = new Array(spectraSize);
// Generate some random data with spectral components
for (let i = 0; i < spectraSize; i++) {
timeData[i] =
2.0 * Math.sin((2 * Math.PI * i) / (20 + n * 0.2)) +
5 * Math.sin((2 * Math.PI * i) / (10 + n * 0.01)) +
10 * Math.sin((2 * Math.PI * i) / (5 + n * -0.002)) +
2.0 * Math.random();
}
// Do a fourier-transform on the data to get the frequency domain
const transform = new Radix2FFT(spectraSize);
const yValues = transform.run(timeData).slice(0, 300); // We only want the first N points just to make the example cleaner
// This is just setting a floor to make the data cleaner for the example
for (let i = 0; i < yValues.length; i++) {
yValues[i] =
yValues[i] < -30 || yValues[i] > -5 ? (yValues[i] < -30 ? -30 : Math.random() * 9 - 6) : yValues[i];
}
yValues[0] = -30;
// we need x-values (sequential numbers) for the frequency data
const xValues = yValues.map((value, index) => index);
return { xValues, yValues };
};
// class CustomOffsetAxis extends NumericAxis {
// constructor(wasmContext: TSciChart, options: INumericAxisOptions) {
// super(wasmContext, options);
// }
// public customOffset: number = 0;
// public get offset(): number {
// return this.customOffset;
// }
// public set offset(value: number) {
// // do nothing
// }
// }
// tslint:disable-next-line:max-classes-per-file
class CrossSectionPaletteProvider extends DefaultPaletteProvider {
public selectedIndex: number = -1;
public shouldUpdate: boolean = true;
public shouldUpdatePalette(): boolean {
return this.shouldUpdate;
}
public overrideStrokeArgb(xValue: number, yValue: number, index: number, opacity: number): number {
if (index === this.selectedIndex || index + 1 === this.selectedIndex || index - 1 === this.selectedIndex) {
return 0xffff8a42;
}
return undefined;
}
}
// This function returns methods for initializing the example
export const getChartsInitializationAPI = () => {
const theme = new SciChartJsNavyTheme();
let mainChartSurface: SciChartSurface;
let mainChartSelectionModifier: SeriesSelectionModifier;
const crossSectionPaletteProvider = new CrossSectionPaletteProvider();
let dragMeAnnotation: CustomAnnotation;
// This function creates the main chart with waterfall series
// To do this, we create N series, each with its own X,Y axis with a different X,Y offset
// all axis other than the first are hidden
const initMainChart = async (rootElement: string | HTMLDivElement) => {
const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
disableAspect: true,
theme,
});
mainChartSurface = sciChartSurface;
const seriesCount = 50;
for (let i = 0; i < seriesCount; i++) {
// Create one yAxis per series
const yAxis = new NumericAxis(wasmContext, {
id: "Y" + i,
axisAlignment: EAxisAlignment.Left,
maxAutoTicks: 5,
drawMinorGridLines: false,
visibleRange: new NumberRange(-60, 60),
isVisible: i === seriesCount - 1,
overrideOffset: 3 * -i,
});
sciChartSurface.yAxes.add(yAxis);
// Create a shared, default xaxis
const xAxis = new NumericAxis(wasmContext, {
id: "X" + i,
axisAlignment: EAxisAlignment.Bottom,
maxAutoTicks: 5,
drawMinorGridLines: false,
growBy: new NumberRange(0, 0.2),
isVisible: i === seriesCount - 1,
overrideOffset: 2 * i,
});
sciChartSurface.xAxes.add(xAxis);
// Create some data for the example
const { xValues, yValues } = createSpectralData(i);
mainChartSurface.rendered.subscribe(() => {
// Don't recalculate the palette unless the selected index changes
crossSectionPaletteProvider.shouldUpdate = false;
});
const lineSeries = new FastLineRenderableSeries(wasmContext, {
id: "S" + i,
xAxisId: "X" + i,
yAxisId: "Y" + i,
stroke: "#64BAE4",
strokeThickness: 1,
dataSeries: new XyDataSeries(wasmContext, { xValues, yValues, dataSeriesName: `Spectra ${i}` }),
paletteProvider: crossSectionPaletteProvider,
});
// Insert series in reverse order so the ones at the bottom of the chart are drawn first
// sciChartSurface.renderableSeries.insert(0, lineSeries);
sciChartSurface.renderableSeries.add(lineSeries);
}
// Add an annotation which can be dragged horizontally to update the bottom right chart
dragMeAnnotation = new CustomAnnotation({
svgString: ``,
x1: 133,
y1: -25,
xAxisId: "X0",
yAxisId: "Y0",
isEditable: true,
annotationsGripsFill: "Transparent",
annotationsGripsStroke: "Transparent",
selectionBoxStroke: "Transparent",
horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
verticalAnchorPoint: EVerticalAnchorPoint.Top,
});
sciChartSurface.annotations.add(dragMeAnnotation);
// Place an annotation with further instructions in the top right of the chart
const promptAnnotation = new CustomAnnotation({
svgString: ``,
xAxisId: "X0",
yAxisId: "Y0",
isEditable: false,
xCoordinateMode: ECoordinateMode.Relative,
yCoordinateMode: ECoordinateMode.Relative,
horizontalAnchorPoint: EHorizontalAnchorPoint.Right,
verticalAnchorPoint: EVerticalAnchorPoint.Top,
x1: 0.9,
y1: 0.1,
});
sciChartSurface.annotations.add(promptAnnotation);
// Add zooming behaviours
sciChartSurface.chartModifiers.add(
new ZoomPanModifier({ xyDirection: EXyDirection.XDirection }),
new MouseWheelZoomModifier({ xyDirection: EXyDirection.XDirection }),
new ZoomExtentsModifier({ xyDirection: EXyDirection.XDirection })
);
const updateSeriesSelectionState = (series: IRenderableSeries) => {
series.stroke = series.isSelected ? "White" : series.isHovered ? "#FFBE93" : "#64BAE4";
series.strokeThickness = series.isSelected || series.isHovered ? 3 : 1;
};
let prevSelectedSeries: IRenderableSeries = sciChartSurface.renderableSeries.get(0);
// Add selection behaviour
mainChartSelectionModifier = new SeriesSelectionModifier({
enableHover: true,
enableSelection: true,
hitTestRadius: 5,
onSelectionChanged: (args) => {
if (args.selectedSeries.length > 0) {
prevSelectedSeries = args.selectedSeries[0];
args.allSeries.forEach(updateSeriesSelectionState);
} else {
prevSelectedSeries.isSelected = true;
}
},
onHoverChanged: (args) => {
args.allSeries.forEach(updateSeriesSelectionState);
},
});
sciChartSurface.chartModifiers.add(mainChartSelectionModifier);
return { sciChartSurface };
};
let crossSectionSelectedSeries: IRenderableSeries;
let crossSectionHoveredSeries: IRenderableSeries;
let crossSectionSliceSeries: XyDataSeries;
let crossSectionLegendModifier: LegendModifier;
// In the bottom left chart, add two series to show the currently hovered/selected series on the main chart
// These will be updated in the selection callback below
const initCrossSectionLeft = async (rootElement: string | HTMLDivElement) => {
const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
disableAspect: true,
theme,
});
sciChartSurface.xAxes.add(
new NumericAxis(wasmContext, {
autoRange: EAutoRange.Always,
drawMinorGridLines: false,
})
);
sciChartSurface.yAxes.add(
new NumericAxis(wasmContext, {
autoRange: EAutoRange.Never,
axisAlignment: EAxisAlignment.Left,
visibleRange: new NumberRange(-30, 5),
drawMinorGridLines: false,
})
);
crossSectionSelectedSeries = new FastLineRenderableSeries(wasmContext, {
stroke: "#ff6600",
strokeThickness: 3,
});
sciChartSurface.renderableSeries.add(crossSectionSelectedSeries);
crossSectionHoveredSeries = new FastMountainRenderableSeries(wasmContext, {
stroke: "#64BAE477",
strokeThickness: 3,
strokeDashArray: [2, 2],
fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
{ color: "#64BAE455", offset: 0 },
{ color: "#64BAE400", offset: 1 },
]),
dataSeries: crossSectionSliceSeries,
zeroLineY: -999,
});
sciChartSurface.renderableSeries.add(crossSectionHoveredSeries);
// Add a legend to the bottom left chart
crossSectionLegendModifier = new LegendModifier({
showCheckboxes: false,
orientation: ELegendOrientation.Horizontal,
});
crossSectionLegendModifier.isEnabled = false;
sciChartSurface.chartModifiers.add(crossSectionLegendModifier);
return { sciChartSurface };
};
const initCrossSectionRight = async (rootElement: string | HTMLDivElement) => {
const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
disableAspect: true,
theme,
title: "Cross Section Slice",
titleStyle: {
fontSize: 13,
padding: Thickness.fromNumber(10),
},
});
sciChartSurface.xAxes.add(
new NumericAxis(wasmContext, {
autoRange: EAutoRange.Always,
drawMinorGridLines: false,
})
);
sciChartSurface.yAxes.add(
new NumericAxis(wasmContext, {
autoRange: EAutoRange.Never,
axisAlignment: EAxisAlignment.Left,
visibleRange: new NumberRange(-30, 5),
drawMinorGridLines: false,
})
);
crossSectionSliceSeries = new XyDataSeries(wasmContext);
sciChartSurface.renderableSeries.add(
new FastMountainRenderableSeries(wasmContext, {
stroke: "#64BAE4",
strokeThickness: 3,
strokeDashArray: [2, 2],
fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
{ color: "#64BAE477", offset: 0 },
{ color: "#64BAE433", offset: 1 },
]),
dataSeries: crossSectionSliceSeries,
zeroLineY: -999,
})
);
return { sciChartSurface };
};
const configureAfterInit = () => {
// Link interactions together
mainChartSelectionModifier.selectionChanged.subscribe((args) => {
const selectedSeries = args.selectedSeries[0]?.dataSeries;
if (selectedSeries) {
crossSectionSelectedSeries.dataSeries = selectedSeries;
}
crossSectionLegendModifier.isEnabled = true;
crossSectionLegendModifier.sciChartLegend?.invalidateLegend();
});
mainChartSelectionModifier.hoverChanged.subscribe((args) => {
const hoveredSeries = args.hoveredSeries[0]?.dataSeries;
if (hoveredSeries) {
crossSectionHoveredSeries.dataSeries = hoveredSeries;
}
crossSectionLegendModifier.sciChartLegend?.invalidateLegend();
});
// Add a function to update drawing the cross-selection when the drag annotation is dragged
const updateDragAnnotation = () => {
// Don't allow to drag vertically, only horizontal
dragMeAnnotation.y1 = -25;
// Find the index to the x-values that the axis marker is on
// Note you could just loop getNativeXValues() here but the wasmContext.NumberUtil function does it for you
const dataIndex = mainChartSurface.webAssemblyContext2D.NumberUtil.FindIndex(
mainChartSurface.renderableSeries.get(0).dataSeries.getNativeXValues(),
dragMeAnnotation.x1,
mainChartSurface.webAssemblyContext2D.SCRTFindIndexSearchMode.Nearest,
true
);
crossSectionPaletteProvider.selectedIndex = dataIndex;
crossSectionPaletteProvider.shouldUpdate = true;
mainChartSurface.invalidateElement();
crossSectionSliceSeries.clear();
for (let i = 0; i < mainChartSurface.renderableSeries.size(); i++) {
crossSectionSliceSeries.append(
i,
mainChartSurface.renderableSeries.get(i).dataSeries.getNativeYValues().get(dataIndex)
);
}
};
// Run it once
updateDragAnnotation();
//Run it when user drags the annotation
dragMeAnnotation.dragDelta.subscribe((args: AnnotationDragDeltaEventArgs) => {
updateDragAnnotation();
});
mainChartSurface.renderableSeries.get(0).isSelected = true;
};
return { initMainChart, initCrossSectionLeft, initCrossSectionRight, configureAfterInit };
};