Skip to main content

Axis APIs – Convert Polar Pixel to Data Coordinates

SciChart.js provides a dedicated API for transforming between polar pixel coordinates and data coordinates, enabling advanced annotations, hit-testing, and custom rendering on polar charts.

Understanding Polar Coordinates in SciChart.js

In polar charts, coordinates are defined by an angle (θ) and a radius (r) from the chart's center, while Cartesian coordinates are defined by (x, y) positions.

Where Polar Pixel Coordinates Are Measured From

  • Origin: The center of the polar chart (not the top-left corner as in Cartesian charts).
  • Angle: Measured from the axis' start angle, increasing in the direction set by the chart configuration.
  • Radius: Measured outward from the center.

All conversions are relative to the series area (viewRect) of the chart.

Converting Polar Data to Cartesian (Pixel) Coordinates

To convert polar data (angle, radius) to Cartesian (pixel) coordinates for rendering or hit-testing, use the annotationHelpers.convertPolarToCartesian()📘 function:

Parameters:

  • angularAxis: The polar angular axis instance.
  • usePixelRatio: Whether to apply device pixel ratio scaling.
  • wasmContext: The WebAssembly context for SciChart.
  • coordinateMode: Indicates if the angle/radius are in pixels, data values, or relative units.
  • angle: The angle in radians
  • radius: The radius in data units

Usage Example

Convert a data-value angle and radius to pixel coordinates for rendering an annotation:

const angularAxis = sciChartSurface.xAxes.get(0) as PolarAxisBase;

const angle = 1.57; // Radians, for example
const radius = 100; // Data units

const { x, y } = convertPolarToCartesian(
angularAxis,
true, // usePixelRatio
wasmContext,
ECoordinateMode.DataValue,
angle,
radius
);
// (x, y) are now pixel coordinates relative to the series area (viewRect)

Practical Applications

  • Hit-Testing: To determine if a mouse event hits a data point or annotation, convert the event's pixel coordinates to polar data coordinates and compare.
  • Custom Rendering: Any custom drawing on a polar chart surface should use this conversion to ensure correct placement.

Hit-Testing in Polar Charts

When hit-testing, convert the mouse (x, y) pixel coordinates to polar coordinates, then compare with data points.

For reference, here is our actual implementation of a hit-test provider for the PolarLineRenderableSeries📘:

import { 
BaseHitTestProvider,
PolarLineRenderableSeries,
PolarAxisBase,
Point,
HitTestInfo,
XyDataSeries,
hitTestHelpers,
annotationHelpers,
ECoordinateMode,
calcDistance, calcDistanceFromLine,
PolarDataPointHitTestProvider,
DpiHelper
} from "scichart";

const DEFAULT_RADIUS = 10;

/**
* Hit-test provider for {@link PolarLineRenderableSeries}. See base class {@link BaseHitTestProvider} for further info
*/
export class PolarLineSeriesHitTestProvider extends PolarDataPointHitTestProvider {
/** @inheritDoc */
public hitTest(x: number, y: number, hitTestRadius: number = DEFAULT_RADIUS): HitTestInfo {
// convert to polar and add necessary offset
const hitTestPoint = this.getTranslatedHitTestPoint(x, y);
if (!hitTestPoint) {
return HitTestInfo.empty();
}

const { xCoordinateCalculator, yCoordinateCalculator, isVerticalChart } = this.currentRenderPassData;
const xHitCoord = hitTestPoint.x;
const yHitCoord = hitTestPoint.y;
const dataSeries = this.parentSeries.dataSeries as XyDataSeries;
if (!dataSeries) {
return HitTestInfo.empty();
}

const xNativeValues = dataSeries.getNativeXValues();
const yNativeValues = dataSeries.getNativeYValues();
const xPolarAxis = this.parentSeries.xAxis as PolarAxisBase;
const getCartesianCoordsFn = (index$: number): Point => {
const x = xNativeValues.get(index$);
const y = yNativeValues.get(index$);
const xCoord = xCoordinateCalculator.getCoordinate(x);
const yCoord = yCoordinateCalculator.getCoordinate(y);
const angle = isVerticalChart ? yCoord : xCoord;
const radius = isVerticalChart ? xCoord : yCoord;
const res = annotationHelpers.convertPolarToCartesian(
xPolarAxis,
false,
this.webAssemblyContext,
ECoordinateMode.Pixel,
angle * DpiHelper.PIXEL_RATIO,
radius
);
return new Point(res.x, res.y);
};
let minDistance = Number.MAX_VALUE;
let minDistanceIndex1 = -1;
let minDistanceIndex2 = -1;
const updateMinDistFn = (dist$: number, ind1$: number, ind2$: number = -1) => {
if (dist$ < minDistance) {
minDistance = dist$;
minDistanceIndex1 = ind1$;
minDistanceIndex2 = ind2$;
}
};
if (dataSeries.count() > 0) {
let point1Coords = getCartesianCoordsFn(0);
let distanceToPoint1 = calcDistance(xHitCoord, yHitCoord, point1Coords.x, point1Coords.y);
updateMinDistFn(distanceToPoint1, 0);
for (let i = 1; i < dataSeries.count(); i++) {
const point2Coords = getCartesianCoordsFn(i);
const lineSegmentLength = calcDistance(point1Coords.x, point1Coords.y, point2Coords.x, point2Coords.y);
const distanceToPoint2 = calcDistance(xHitCoord, yHitCoord, point2Coords.x, point2Coords.y);
updateMinDistFn(distanceToPoint2, i);
const isHitPointInLineSegmentVicinity =
distanceToPoint1 < lineSegmentLength && distanceToPoint2 < lineSegmentLength;
if (isHitPointInLineSegmentVicinity) {
const distanceToLine = calcDistanceFromLine(
xHitCoord,
yHitCoord,
point1Coords.x,
point1Coords.y,
point2Coords.x,
point2Coords.y
);
if (distanceToLine < minDistance) {
if (distanceToPoint1 < distanceToPoint2) {
updateMinDistFn(distanceToLine, i - 1, i);
} else {
updateMinDistFn(distanceToLine, i, i - 1);
}
}
}
point1Coords = point2Coords;
distanceToPoint1 = distanceToPoint2;
}
}

const polarHitTestPoint = xPolarAxis.reverseTransform(hitTestPoint.x, hitTestPoint.y);

const hitTestInfo = hitTestHelpers.createHitTestInfo(
this.parentSeries,
xCoordinateCalculator,
yCoordinateCalculator,
isVerticalChart,
dataSeries,
xNativeValues,
yNativeValues,
polarHitTestPoint.x,
polarHitTestPoint.y,
minDistanceIndex1,
hitTestRadius
);
hitTestInfo.isHit = minDistance < hitTestRadius;
return hitTestInfo;
}
}
note

The inverse function of convertPolarToCartesian() is reverseTransform()📘, which converts from cartesian to polar coordinates.