diff --git a/.gitignore b/.gitignore index b287891d0..a6a558584 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,6 @@ Examples/exampleInfo.* .aider* Examples/logs Examples/logs/all.log - +.sciclawd +.claude /docs/ \ No newline at end of file diff --git a/Examples/package-lock.json b/Examples/package-lock.json index 12c4af989..41a4ddb9a 100644 --- a/Examples/package-lock.json +++ b/Examples/package-lock.json @@ -37,6 +37,7 @@ "morgan": "^1.10.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-draggable": "^4.5.0", "react-fast-compare": "^3.2.2", "react-helmet": "^6.1.0", "react-markdown": "^9.0.3", @@ -10514,6 +10515,20 @@ "react": "^19.2.3" } }, + "node_modules/react-draggable": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", diff --git a/Examples/package.json b/Examples/package.json index a3b33f5b1..2f3b87a00 100644 --- a/Examples/package.json +++ b/Examples/package.json @@ -65,6 +65,7 @@ "morgan": "^1.10.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-draggable": "^4.5.0", "react-fast-compare": "^3.2.2", "react-helmet": "^6.1.0", "react-markdown": "^9.0.3", diff --git a/Examples/scripts/enforce-trailing-slashes.js b/Examples/scripts/enforce-trailing-slashes.js index 601ed5ee6..e3040a23b 100644 --- a/Examples/scripts/enforce-trailing-slashes.js +++ b/Examples/scripts/enforce-trailing-slashes.js @@ -27,7 +27,7 @@ function processFile(filePath) { fs.writeFileSync(filePath, newContent, "utf8"); console.log("- Updated:", filePath); } else { - // console.log("- No changes needed:", filePath); + // console.log("- No changes needed:", filePath); } } diff --git a/Examples/scripts/linkcheck.js b/Examples/scripts/linkcheck.js index 782101931..7f5cecdcb 100644 --- a/Examples/scripts/linkcheck.js +++ b/Examples/scripts/linkcheck.js @@ -1,13 +1,13 @@ -const fs = require('fs'); -const path = require('path'); -const https = require('https'); -const http = require('http'); -const { URL } = require('url'); +const fs = require("fs"); +const path = require("path"); +const https = require("https"); +const http = require("http"); +const { URL } = require("url"); // This script checks all links in markdown files within the 'Examples' directory and its subdirectories. // To run: "npm run linkcheck" -const examplesDir = path.join(__dirname, '.', '../src'); +const examplesDir = path.join(__dirname, ".", "../src"); const linkRegex = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g; // markdown links: [text](url) let fileCount = 0; @@ -15,11 +15,11 @@ let linkCount = 0; let errorCount = 0; function walkDir(dir, callback) { - fs.readdirSync(dir).forEach(file => { + fs.readdirSync(dir).forEach((file) => { const fullPath = path.join(dir, file); if (fs.statSync(fullPath).isDirectory()) { walkDir(fullPath, callback); - } else if (fullPath.endsWith('.md') || fullPath.endsWith('.mdx') || fullPath.endsWith('.tsx')) { + } else if (fullPath.endsWith(".md") || fullPath.endsWith(".mdx") || fullPath.endsWith(".tsx")) { callback(fullPath); } }); @@ -27,27 +27,29 @@ function walkDir(dir, callback) { function checkLink(url) { return new Promise((resolve) => { - let lib = url.startsWith('https') ? https : http; + let lib = url.startsWith("https") ? https : http; let req; try { - req = lib.request(url, { method: 'HEAD', timeout: 5000 }, res => { + req = lib.request(url, { method: "HEAD", timeout: 5000 }, (res) => { resolve({ url, status: res.statusCode }); }); - req.on('error', () => { + req.on("error", () => { // Try GET if HEAD fails (some servers don't support HEAD) - req = lib.get(url, { timeout: 5000 }, res => { - resolve({ url, status: res.statusCode }); - }).on('error', () => { - resolve({ url, status: 'ERR' }); - }); + req = lib + .get(url, { timeout: 5000 }, (res) => { + resolve({ url, status: res.statusCode }); + }) + .on("error", () => { + resolve({ url, status: "ERR" }); + }); }); - req.on('timeout', () => { + req.on("timeout", () => { req.destroy(); - resolve({ url, status: 'TIMEOUT' }); + resolve({ url, status: "TIMEOUT" }); }); req.end(); } catch (e) { - resolve({ url, status: 'ERR' }); + resolve({ url, status: "ERR" }); } }); } @@ -55,7 +57,7 @@ function checkLink(url) { async function checkLinksInFile(filePath) { fileCount++; - const content = fs.readFileSync(filePath, 'utf8'); + const content = fs.readFileSync(filePath, "utf8"); const links = []; let match; while ((match = linkRegex.exec(content)) !== null) { @@ -75,10 +77,10 @@ async function checkLinksInFile(filePath) { if (status === 404) { console.log(` ❌ 404 Not Found: ${url} (in file: ${filePath})`); errorCount++; - } else if (status === 'ERR') { + } else if (status === "ERR") { console.log(` ⚠️ Error: ${url}`); errorCount++; - } else if (status === 'TIMEOUT') { + } else if (status === "TIMEOUT") { console.log(` Timeout: ${url}`); errorCount++; } else { @@ -93,16 +95,16 @@ async function checkLinksInFile(filePath) { console.log('Starting link checking in "/Examples/src" directory...'); const files = []; - walkDir(examplesDir, file => files.push(file)); + walkDir(examplesDir, (file) => files.push(file)); for (const file of files) { await checkLinksInFile(file); } - console.log('\nLink check complete.'); + console.log("\nLink check complete."); console.log(`Total files checked: ${fileCount}`); console.log(`Total links checked: ${linkCount}\n`); if (errorCount > 0) { console.error(`\x1b[31mTotal errors found: ${errorCount}\x1b[0m`); } else { - console.log('\x1b[32mAll links are valid!\x1b[0m'); + console.log("\x1b[32mAll links are valid!\x1b[0m"); } })(); diff --git a/Examples/src/components/AppFooter/AppFooter.module.scss b/Examples/src/components/AppFooter/AppFooter.module.scss index 8f26e8e29..a597ce868 100644 --- a/Examples/src/components/AppFooter/AppFooter.module.scss +++ b/Examples/src/components/AppFooter/AppFooter.module.scss @@ -16,8 +16,8 @@ .FooterBottomSection { @extend %CommonFooterGridConfigs; } - - h3, + + h3, h4, h5, h6 { diff --git a/Examples/src/components/AppRouter/examplePaths.ts b/Examples/src/components/AppRouter/examplePaths.ts index 7451b5a51..5fb367a81 100644 --- a/Examples/src/components/AppRouter/examplePaths.ts +++ b/Examples/src/components/AppRouter/examplePaths.ts @@ -14,7 +14,6 @@ export default [ "../Examples/Charts2D/AxisLabelCustomization/MultiLineLabels", "../Examples/Charts2D/AxisLabelCustomization/RotatedLabels", "../Examples/Charts2D/BasicChartTypes/BandSeriesChart", - "../Examples/Charts2D/v4Charts/HeatmapOverMap", "../Examples/Charts2D/BasicChartTypes/BubbleChart", "../Examples/Charts2D/BasicChartTypes/CandlestickChart", "../Examples/Charts2D/BasicChartTypes/ColumnChart", @@ -61,9 +60,9 @@ export default [ "../Examples/Charts2D/Filters/PercentageChange", "../Examples/Charts2D/Filters/TrendMARatio", "../Examples/Charts2D/Legends/ChartLegendsAPI", - "../Examples/Charts2D/ModifyAxisBehavior/DiscontinuousDateAxisComparison", "../Examples/Charts2D/ModifyAxisBehavior/BaseValueAxes", "../Examples/Charts2D/ModifyAxisBehavior/CentralAxes", + "../Examples/Charts2D/ModifyAxisBehavior/DiscontinuousDateAxisComparison", "../Examples/Charts2D/ModifyAxisBehavior/DrawBehindAxes", "../Examples/Charts2D/ModifyAxisBehavior/LogarithmicAxis", "../Examples/Charts2D/ModifyAxisBehavior/MultipleXAxes", @@ -129,6 +128,7 @@ export default [ "../Examples/Charts2D/v4Charts/AnimatedColumns", "../Examples/Charts2D/v4Charts/BoxPlotChart", "../Examples/Charts2D/v4Charts/GanttChart", + "../Examples/Charts2D/v4Charts/HeatmapOverMap", "../Examples/Charts2D/v4Charts/HeatmapRectangle", "../Examples/Charts2D/v4Charts/HistogramChart", "../Examples/Charts2D/v4Charts/LinearGauges", @@ -161,6 +161,7 @@ export default [ "../Examples/FeaturedApps/ScientificCharts/LiDAR3DPointCloudDemo", "../Examples/FeaturedApps/ScientificCharts/PhasorDiagramChart", "../Examples/FeaturedApps/ScientificCharts/Semiconductors", + "../Examples/FeaturedApps/ScientificCharts/SmithChart", "../Examples/FeaturedApps/ScientificCharts/TenorCurves3D", "../Examples/FeaturedApps/ScientificCharts/WaferAnalysis", "../Examples/FeaturedApps/ShowCases/DynamicLayout", diff --git a/Examples/src/components/AppRouter/examples.ts b/Examples/src/components/AppRouter/examples.ts index 8036aa658..c2bc13075 100644 --- a/Examples/src/components/AppRouter/examples.ts +++ b/Examples/src/components/AppRouter/examples.ts @@ -41,6 +41,7 @@ export const MENU_ITEMS_FEATURED_APPS: TMenuItem[] = [ EXAMPLES_PAGES.featuredApps_scientificCharts_PhasorDiagramChart, EXAMPLES_PAGES.featuredApps_scientificCharts_CorrelationPlot, EXAMPLES_PAGES.featuredApps_scientificCharts_Semiconductors, + EXAMPLES_PAGES.featuredApps_scientificCharts_SmithChart, EXAMPLES_PAGES.featuredApps_scientificCharts_WaferAnalysis, ], }, diff --git a/Examples/src/components/AppTopBar/AppBarTop.tsx b/Examples/src/components/AppTopBar/AppBarTop.tsx index c98576494..38ff7e89f 100644 --- a/Examples/src/components/AppTopBar/AppBarTop.tsx +++ b/Examples/src/components/AppTopBar/AppBarTop.tsx @@ -35,11 +35,7 @@ const AppBarTop: FC = (props) => { theme === ETheme.navy ? "SciChartNavy" : theme === ETheme.light ? "SciChartLight" : "SciChartDark"; const nextTheme = getNextTheme(theme); const nextThemeLabel = - nextTheme === ETheme.navy - ? "SciChartNavy" - : nextTheme === ETheme.light - ? "SciChartLight" - : "SciChartDark"; + nextTheme === ETheme.navy ? "SciChartNavy" : nextTheme === ETheme.light ? "SciChartLight" : "SciChartDark"; const baseGithubPath = "https://github.com/ABTSoftware/SciChart.JS.Examples/blob/master/Examples/src"; const contextualGithub = @@ -93,8 +89,8 @@ const AppBarTop: FC = (props) => { - )}. -
+ )} + .
- { const { sciChartSurface, wasmContext } = await SciChartPolarSurface.create(rootElement, { @@ -68,7 +73,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { title: "Cunsumer prices relative to past year in UK, 2024", titleStyle: { fontSize: 24, - } + }, }); const radialYAxis = new PolarNumericAxis(wasmContext, { @@ -112,14 +117,14 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { startAngle: Math.PI / 2, autoTicks: false, majorDelta: 1, - labels: DATA_UK.labels + labels: DATA_UK.labels, }); sciChartSurface.xAxes.add(polarXAxis); const polarColumn = new PolarColumnRenderableSeries(wasmContext, { dataSeries: new XyDataSeries(wasmContext, { xValues: Array.from({ length: DATA_UK.data.length }, (_, i) => i), - yValues: DATA_UK.data + yValues: DATA_UK.data, }), dataLabels: { style: { @@ -132,7 +137,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { }, dataPointWidth: 0.6, strokeThickness: 2, - paletteProvider: new ColumnPaletteProvider(0), + paletteProvider: new ColumnPaletteProvider(0), animation: new WaveAnimation({ duration: 800, zeroLine: 0, fadeEffect: true }), }); sciChartSurface.renderableSeries.add(polarColumn); @@ -144,4 +149,4 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { ); return { sciChartSurface, wasmContext }; -}; \ No newline at end of file +}; diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarColumnChart/README.md b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarColumnChart/README.md index dd63b20a6..ba4152526 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarColumnChart/README.md +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarColumnChart/README.md @@ -6,83 +6,92 @@ This example demonstrates how to create a **JavaScript Polar Column Chart** usin ## Technologies Used -- SciChart.js – High performance WebGL charting library -- TypeScript – For type-safe implementation -- WebGL – For hardware-accelerated rendering +- SciChart.js – High performance WebGL charting library +- TypeScript – For type-safe implementation +- WebGL – For hardware-accelerated rendering ## Code Explanation The example is built around the `drawExample` function which initializes a `SciChartPolarSurface` and configures it with polar axes and a column series: -1. **Surface Initialization**: - - Creates a polar chart surface with `SciChartPolarSurface.create()` - - Sets theme and drawing options (`drawSeriesBehindAxis: true`) +1. **Surface Initialization**: + + - Creates a polar chart surface with `SciChartPolarSurface.create()` + - Sets theme and drawing options (`drawSeriesBehindAxis: true`) 2. **Axes Configuration**: - - **Radial Y-Axis**: - - Uses `PolarNumericAxis` in `EPolarAxisMode.Radial` - - Customized with no tick lines, grid styling, and inner radius for donut effect - - **Angular X-Axis**: - - Uses `PolarNumericAxis` in `EPolarAxisMode.Angular` - - Configured with parallel labels, clockwise orientation, and custom styling + + - **Radial Y-Axis**: + - Uses `PolarNumericAxis` in `EPolarAxisMode.Radial` + - Customized with no tick lines, grid styling, and inner radius for donut effect + - **Angular X-Axis**: + - Uses `PolarNumericAxis` in `EPolarAxisMode.Angular` + - Configured with parallel labels, clockwise orientation, and custom styling 3. **Data Series**: - - Uses `XyDataSeries` to provide x,y values for the columns - - Rendered via `PolarColumnRenderableSeries` with: - - Gradient fill using `GradientParams` - - White borders and custom width - - Parallel data labels - - WaveAnimation for smooth entry + + - Uses `XyDataSeries` to provide x,y values for the columns + - Rendered via `PolarColumnRenderableSeries` with: + - Gradient fill using `GradientParams` + - White borders and custom width + - Parallel data labels + - WaveAnimation for smooth entry 4. **Interactivity**: - - Adds polar-specific modifiers: - - `PolarPanModifier` for panning - - `PolarZoomExtentsModifier` for zoom-to-fit - - `PolarMouseWheelZoomModifier` for zooming + - Adds polar-specific modifiers: + - `PolarPanModifier` for panning + - `PolarZoomExtentsModifier` for zoom-to-fit + - `PolarMouseWheelZoomModifier` for zooming ## Customization Key customization points in this example include: 1. **Gradient Styling**: - - The columns use a multi-stop linear gradient from `DarkIndigo` to `MutedBlue` - - Gradient direction is horizontal (can be made vertical with `new Point(0, 1)`) + + - The columns use a multi-stop linear gradient from `DarkIndigo` to `MutedBlue` + - Gradient direction is horizontal (can be made vertical with `new Point(0, 1)`) 2. **Polar Layout**: - - `startAngle: Math.PI / 2` begins the chart at 12 o'clock - - `flippedCoordinates: true` makes values progress clockwise - - `innerRadius: 0.1` creates a donut hole effect + + - `startAngle: Math.PI / 2` begins the chart at 12 o'clock + - `flippedCoordinates: true` makes values progress clockwise + - `innerRadius: 0.1` creates a donut hole effect 3. **Animation**: - - `WaveAnimation` with 800ms duration and fade effect - - Animates columns growing from the center outward + + - `WaveAnimation` with 800ms duration and fade effect + - Animates columns growing from the center outward 4. **Label Positioning**: - - `EPolarLabelMode.Parallel` keeps data labels aligned with columns - - Custom font styling for improved readability + - `EPolarLabelMode.Parallel` keeps data labels aligned with columns + - Custom font styling for improved readability ## Running the Example To run this example from the SciChart.JS.Examples repository: 1. **Clone the Repository**: - ```bash - git clone https://github.com/ABTSoftware/SciChart.JS.Examples.git - ``` + + ```bash + git clone https://github.com/ABTSoftware/SciChart.JS.Examples.git + ``` 2. **Navigate to Examples**: - ```bash - cd SciChart.JS.Examples/Examples - ``` + + ```bash + cd SciChart.JS.Examples/Examples + ``` 3. **Install Dependencies**: - ```bash - npm install - ``` + + ```bash + npm install + ``` 4. **Run Development Server**: - ```bash - npm run dev - ``` + ```bash + npm run dev + ``` -For more details, refer to the [SciChart.JS.Examples README](https://github.com/ABTSoftware/SciChart.JS.Examples/blob/master/README.md) and [Polar Chart Documentation](https://www.scichart.com/documentation/js/v5/2d-charts/surface/scichart-polar-surface-type/). \ No newline at end of file +For more details, refer to the [SciChart.JS.Examples README](https://github.com/ABTSoftware/SciChart.JS.Examples/blob/master/README.md) and [Polar Chart Documentation](https://www.scichart.com/documentation/js/v5/2d-charts/surface/scichart-polar-surface-type/). diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarColumnChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarColumnChart/drawExample.ts index 112f0cc7b..2eb55c865 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarColumnChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarColumnChart/drawExample.ts @@ -6,11 +6,11 @@ import { XyDataSeries, PolarNumericAxis, SciChartPolarSurface, - EPolarAxisMode, - NumberRange, - EAxisAlignment, - GradientParams, - Point, + EPolarAxisMode, + NumberRange, + EAxisAlignment, + GradientParams, + Point, EPolarLabelMode, WaveAnimation, } from "scichart"; @@ -19,7 +19,7 @@ import { appTheme } from "../../../theme"; export const drawExample = async (rootElement: string | HTMLDivElement) => { const { sciChartSurface, wasmContext } = await SciChartPolarSurface.create(rootElement, { theme: appTheme.SciChartJsTheme, - drawSeriesBehindAxis: true + drawSeriesBehindAxis: true, }); const radialYAxis = new PolarNumericAxis(wasmContext, { @@ -27,7 +27,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { axisAlignment: EAxisAlignment.Right, visibleRange: new NumberRange(0, 6), zoomExtentsToInitialRange: true, - + drawMinorTickLines: false, drawMajorTickLines: false, drawMinorGridLines: false, @@ -59,16 +59,16 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { const polarColumn = new PolarColumnRenderableSeries(wasmContext, { dataSeries: new XyDataSeries(wasmContext, { xValues: [0, 1, 2, 3, 4, 5, 6, 7, 8], - yValues: [2.6, 5.3, 3.5, 2.7, 4.8, 3.8, 5, 4.5, 3.5] + yValues: [2.6, 5.3, 3.5, 2.7, 4.8, 3.8, 5, 4.5, 3.5], }), fillLinearGradient: new GradientParams( - new Point(0, 0), + new Point(0, 0), new Point(1, 0), // `new Point(0, 1)` for vertical gradient [ { color: appTheme.DarkIndigo, offset: 0 }, { color: appTheme.Indigo, offset: 0.2 }, { color: appTheme.Indigo, offset: 0.8 }, - { color: appTheme.MutedBlue, offset: 1 } + { color: appTheme.MutedBlue, offset: 1 }, ] ), // stroke: "white", @@ -93,4 +93,4 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { ); return { sciChartSurface, wasmContext }; -}; \ No newline at end of file +}; diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarGaugeChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarGaugeChart/drawExample.ts index 106474e96..f4d3edbb8 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarGaugeChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarGaugeChart/drawExample.ts @@ -46,7 +46,7 @@ export const getChartsInitializationAPI = () => { axisAlignment: EAxisAlignment.Right, // start labels in sync with angular axis - startAngle: - Math.PI / 4, + startAngle: -Math.PI / 4, drawLabels: false, drawMinorGridLines: false, @@ -65,7 +65,7 @@ export const getChartsInitializationAPI = () => { flippedCoordinates: true, useNativeText: true, totalAngle: (Math.PI * 3) / 2, - startAngle: - Math.PI / 4, + startAngle: -Math.PI / 4, drawMinorGridLines: false, drawMajorGridLines: false, @@ -238,7 +238,7 @@ export const getChartsInitializationAPI = () => { flippedCoordinates: true, useNativeText: true, totalAngle: (Math.PI * 3) / 2, - startAngle: - Math.PI / 4, + startAngle: -Math.PI / 4, autoTicks: false, majorDelta: 10, @@ -369,7 +369,7 @@ export const getChartsInitializationAPI = () => { visibleRange: new NumberRange(0, 10), zoomExtentsToInitialRange: true, - startAngle: - Math.PI / 4, + startAngle: -Math.PI / 4, drawLabels: false, drawMinorGridLines: false, @@ -393,7 +393,7 @@ export const getChartsInitializationAPI = () => { flippedCoordinates: true, useNativeText: true, totalAngle: (Math.PI * 3) / 2, - startAngle: - Math.PI / 4, + startAngle: -Math.PI / 4, drawMinorGridLines: false, drawMajorGridLines: false, diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLabelMode/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLabelMode/drawExample.ts index d097248a4..b1308cee3 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLabelMode/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLabelMode/drawExample.ts @@ -6,7 +6,7 @@ import { XyDataSeries, PolarLineRenderableSeries, EllipsePointMarker, - PolarNumericAxis, + PolarNumericAxis, EPolarAxisMode, NumberRange, EPolarLabelMode, @@ -45,7 +45,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { zoomExtentsToInitialRange: true, labelStyle: { - fontSize: 16 + fontSize: 16, }, }); sciChartSurface.xAxes.add(angularXAxis); @@ -58,7 +58,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { }), pointMarker: new EllipsePointMarker(wasmContext), stroke: appTheme.VividOrange, - strokeThickness: 3 + strokeThickness: 3, }); sciChartSurface.renderableSeries.add(polarlineSeries); @@ -73,15 +73,15 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { angularXAxis.polarLabelMode = newMode; } - return { - sciChartSurface, - wasmContext, - controls: { + return { + sciChartSurface, + wasmContext, + controls: { changePolarLabelMode, toggleIsInnerAxis: (isInnerAxis: boolean) => { angularXAxis.isInnerAxis = isInnerAxis; radialYAxis.isInnerAxis = isInnerAxis; - } - } + }, + }, }; -}; \ No newline at end of file +}; diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLabelMode/index.tsx b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLabelMode/index.tsx index ecd132567..13b1cc7e0 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLabelMode/index.tsx +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLabelMode/index.tsx @@ -12,10 +12,10 @@ export default function ChartComponent() { const [preset, setPreset] = useState(EPolarLabelMode.Horizontal); const [isInnerAxis, setIsInnerAxis] = useState(false); - const [controls, setControls] = useState({ + const [controls, setControls] = useState({ changePolarLabelMode: (newMode: EPolarLabelMode) => {}, toggleIsInnerAxis: (isInnerAxis: boolean) => {}, - }) + }); const handleToggleButtonChanged = (event: any, value: EPolarLabelMode) => { if (value === null) return; @@ -30,13 +30,15 @@ export default function ChartComponent() { return (
-
+
+ > {Object.keys(EPolarLabelMode).map((key) => ( - + {key} ))} -
@@ -73,5 +68,5 @@ export default function ChartComponent() { />
- ) + ); } diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLineMultiCycle/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLineMultiCycle/drawExample.ts index c4b95178d..f0702aef7 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLineMultiCycle/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarLineMultiCycle/drawExample.ts @@ -160,7 +160,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { drawLabels: true, labelPrecision: 0, labelStyle: { - color: appTheme.TextColor + color: appTheme.TextColor, }, labelPostfix: "°C", autoTicks: false, diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMountainChart/README.md b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMountainChart/README.md index aaf6a1e4e..d74d4bacf 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMountainChart/README.md +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMountainChart/README.md @@ -6,17 +6,18 @@ This example demonstrates how to create a **JavaScript Polar Mountain Chart** us ## Technologies Used -- SciChart.js – High performance WebGL charting library -- Polar chart components (PolarSurface, PolarNumericAxis) -- Polar-specific renderable series (PolarMountainRenderableSeries) -- Polar chart modifiers (PolarZoomExtentsModifier, PolarLegendModifier) -- Animation effects (WaveAnimation) +- SciChart.js – High performance WebGL charting library +- Polar chart components (PolarSurface, PolarNumericAxis) +- Polar-specific renderable series (PolarMountainRenderableSeries) +- Polar chart modifiers (PolarZoomExtentsModifier, PolarLegendModifier) +- Animation effects (WaveAnimation) ## Code Explanation The example centers around the `drawExample` function which creates a polar chart surface with radial and angular axes. Key components include: -1. **Polar Surface Initialization**: +1. **Polar Surface Initialization**: + ```javascript const { sciChartSurface, wasmContext } = await SciChartPolarSurface.create(rootElement, { theme: appTheme.SciChartJsTheme, @@ -24,6 +25,7 @@ const { sciChartSurface, wasmContext } = await SciChartPolarSurface.create(rootE ``` 2. **Radial Y-Axis Configuration**: + ```javascript const radialYAxis = new PolarNumericAxis(wasmContext, { polarAxisMode: EPolarAxisMode.Radial, @@ -34,6 +36,7 @@ const radialYAxis = new PolarNumericAxis(wasmContext, { ``` 3. **Angular X-Axis Configuration**: + ```javascript const polarXAxis = new PolarNumericAxis(wasmContext, { polarAxisMode: EPolarAxisMode.Angular, @@ -43,6 +46,7 @@ const polarXAxis = new PolarNumericAxis(wasmContext, { ``` 4. **Mountain Series Creation**: + ```javascript const polarMountain = new PolarMountainRenderableSeries(wasmContext, { dataSeries: new XyDataSeries(wasmContext, { @@ -56,6 +60,7 @@ const polarMountain = new PolarMountainRenderableSeries(wasmContext, { ``` 5. **Polar Chart Modifiers**: + ```javascript sciChartSurface.chartModifiers.add( new PolarPanModifier(), @@ -70,31 +75,31 @@ sciChartSurface.chartModifiers.add( Key customization features in this example include: 1. **Closed-Loop Mountain Series**: The example demonstrates how to create continuous polar mountain charts by duplicating the first point at the end of the data series: + ```javascript xValues: [...xValues, xValues[xValues.length - 1] + 1], yValues: [...yValues, yValues[0]] ``` 2. **Line Interpolation**: The `interpolateLine` property allows switching between straight line segments and curved interpolation: + ```javascript -interpolateLine: true // Creates smooth curves between points +interpolateLine: true; // Creates smooth curves between points ``` 3. **Polar Gradient Fills**: Custom gradient fills are applied with transparency: + ```javascript -fillLinearGradient: new GradientParams( - new Point(0, 0), - new Point(0, 1), - [ - { color: fillColor + "AA", offset: 0 }, - { color: fillColor + "33", offset: 0.3 }, - ] -) +fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [ + { color: fillColor + "AA", offset: 0 }, + { color: fillColor + "33", offset: 0.3 }, +]); ``` 4. **Clockwise Coordinate System**: The polar chart is configured to render clockwise with: + ```javascript -flippedCoordinates: true +flippedCoordinates: true; ``` 5. **Polar-Specific Modifiers**: The example includes polar-optimized versions of common modifiers like `PolarZoomExtentsModifier` and `PolarLegendModifier`. @@ -104,23 +109,27 @@ flippedCoordinates: true To run this example from the SciChart.JS.Examples repository: 1. **Clone the Repository**: + ```bash git clone https://github.com/ABTSoftware/SciChart.JS.Examples.git ``` 2. **Navigate to the Examples Directory**: + ```bash cd SciChart.JS.Examples/Examples ``` 3. **Install Dependencies**: + ```bash npm install ``` 4. **Run the Development Server**: + ```bash npm run dev ``` -For more information on polar charts, refer to the [SciChart.js Polar Charts Documentation](https://www.scichart.com/documentation/js/v5/2d-charts/surface/scichart-polar-surface-type/). \ No newline at end of file +For more information on polar charts, refer to the [SciChart.js Polar Charts Documentation](https://www.scichart.com/documentation/js/v5/2d-charts/surface/scichart-polar-surface-type/). diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMountainChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMountainChart/drawExample.ts index 79046bcb1..c27beda10 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMountainChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMountainChart/drawExample.ts @@ -5,15 +5,15 @@ import { XyDataSeries, PolarNumericAxis, SciChartPolarSurface, - EPolarAxisMode, - NumberRange, - EAxisAlignment, - GradientParams, - Point, + EPolarAxisMode, + NumberRange, + EAxisAlignment, + GradientParams, + Point, EPolarLabelMode, WaveAnimation, PolarMountainRenderableSeries, - PolarLegendModifier + PolarLegendModifier, } from "scichart"; import { appTheme } from "../../../theme"; @@ -34,7 +34,7 @@ const MountainsDatasets = [ fillColor: appTheme.VividPink, interpolateLine: false, }, -] +]; export const drawExample = async (rootElement: string | HTMLDivElement) => { const { sciChartSurface, wasmContext } = await SciChartPolarSurface.create(rootElement, { @@ -68,28 +68,24 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { }); sciChartSurface.xAxes.add(polarXAxis); - MountainsDatasets.forEach(({yValues, fillColor, interpolateLine}) => { + MountainsDatasets.forEach(({ yValues, fillColor, interpolateLine }) => { const polarMountain = new PolarMountainRenderableSeries(wasmContext, { dataSeries: new XyDataSeries(wasmContext, { xValues: [...xValues, xValues[xValues.length - 1] + 1], // add 1 more xValue to close the loop yValues: [...yValues, yValues[0]], // close the loop by drawing to the first yValue dataSeriesName: interpolateLine ? "Interpolated" : "Straight", }), - fillLinearGradient: new GradientParams( - new Point(0, 0), - new Point(0, 1), - [ - { color: fillColor + "AA", offset: 0 }, - { color: fillColor + "33", offset: 0.3 }, - ] - ), + fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [ + { color: fillColor + "AA", offset: 0 }, + { color: fillColor + "33", offset: 0.3 }, + ]), interpolateLine: interpolateLine, stroke: fillColor, // this also gives off the color for the legend marker strokeThickness: 2, animation: new WaveAnimation({ duration: 800, zeroLine: 0 }), }); sciChartSurface.renderableSeries.add(polarMountain); - }) + }); sciChartSurface.chartModifiers.add( new PolarPanModifier(), @@ -101,4 +97,4 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { ); return { sciChartSurface, wasmContext }; -}; \ No newline at end of file +}; diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMultipleRadialAxesRadarChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMultipleRadialAxesRadarChart/drawExample.ts index 30142ea18..2fbb73bf2 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMultipleRadialAxesRadarChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarMultipleRadialAxesRadarChart/drawExample.ts @@ -5,9 +5,9 @@ import { XyDataSeries, PolarNumericAxis, SciChartPolarSurface, - EColor, - EPolarAxisMode, - EPolarGridlineMode, + EColor, + EPolarAxisMode, + EPolarGridlineMode, PolarCategoryAxis, ENumericFormat, EPolarLabelMode, @@ -19,32 +19,25 @@ import { } from "scichart"; import { appTheme } from "../../../theme"; -const LABELS = [ - "Complexity", - "Memory Usage", - "Stability", - "Adaptability", - "Scalability", - "Cache Efficiency" -]; +const LABELS = ["Complexity", "Memory Usage", "Stability", "Adaptability", "Scalability", "Cache Efficiency"]; const DATA_SET = [ { name: "Quick Sort", color: appTheme.VividSkyBlue, - values: [7, 8, 2, 8, 9, 9] + values: [7, 8, 2, 8, 9, 9], }, - { + { name: "Bubble Sort", color: appTheme.VividOrange, - values: [2, 9, 10, 5, 1, 2], + values: [2, 9, 10, 5, 1, 2], }, -] +]; // this chart expresses the complexity, memory usage, stability, adaptability, scalability, and cache efficiency of two sorting algorithms export const drawExample = async (rootElement: string | HTMLDivElement) => { const { sciChartSurface, wasmContext } = await SciChartPolarSurface.create(rootElement, { - theme: appTheme.SciChartJsTheme + theme: appTheme.SciChartJsTheme, }); const radialYAxis = new PolarNumericAxis(wasmContext, { @@ -57,7 +50,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { majorGridLineStyle: { color: EColor.BackgroundColor, strokeThickness: 1, - strokeDashArray: [5, 5] + strokeDashArray: [5, 5], }, labelStyle: { fontSize: 16, @@ -67,9 +60,9 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { drawMajorTickLines: false, drawMinorTickLines: false, startAngle: Math.PI / 2, // start at 12 o'clock - innerRadius: 0, + innerRadius: 0, }); - sciChartSurface.yAxes.add(radialYAxis); + sciChartSurface.yAxes.add(radialYAxis); const angularXAxis = new PolarCategoryAxis(wasmContext, { polarAxisMode: EPolarAxisMode.Angular, @@ -80,7 +73,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { majorGridLineStyle: { color: EColor.BackgroundColor, strokeThickness: 1, - strokeDashArray: [5, 5] + strokeDashArray: [5, 5], }, flippedCoordinates: true, // go clockwise drawMinorGridLines: false, @@ -91,19 +84,19 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { angularXAxis.polarLabelMode = EPolarLabelMode.Parallel; sciChartSurface.xAxes.add(angularXAxis); - const xValues = Array.from({ length: LABELS.length + 1 }, (_, i) => i); + const xValues = Array.from({ length: LABELS.length + 1 }, (_, i) => i); // +1 to complete the radar chart without overlap of first and last labels - + const polarMountain = new PolarMountainRenderableSeries(wasmContext, { dataSeries: new XyDataSeries(wasmContext, { xValues: xValues, yValues: [...DATA_SET[0].values, DATA_SET[0].values[0]], // +1 append first value to complete the radar chart - dataSeriesName: DATA_SET[0].name + dataSeriesName: DATA_SET[0].name, }), stroke: DATA_SET[0].color, fill: DATA_SET[0].color + "30", strokeThickness: 4, - animation: new FadeAnimation({ duration: 1000 }) + animation: new FadeAnimation({ duration: 1000 }), }); sciChartSurface.renderableSeries.add(polarMountain); @@ -112,7 +105,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { dataSeries: new XyDataSeries(wasmContext, { xValues: xValues, yValues: [...DATA_SET[1].values, DATA_SET[1].values[0]], // +1 append first value to complete the radar chart - dataSeriesName: DATA_SET[1].name + dataSeriesName: DATA_SET[1].name, }), stroke: DATA_SET[1].color, strokeThickness: 4, @@ -123,7 +116,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { fill: DATA_SET[1].color, stroke: EColor.White, }), - animation: new FadeAnimation({ duration: 1000 }) + animation: new FadeAnimation({ duration: 1000 }), }); sciChartSurface.renderableSeries.add(polarLine); @@ -131,7 +124,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { new PolarPanModifier(), new PolarZoomExtentsModifier(), new PolarMouseWheelZoomModifier({ growFactor: 0.0002 }), - new PolarLegendModifier({ showSeriesMarkers: true, showCheckboxes: true }), + new PolarLegendModifier({ showSeriesMarkers: true, showCheckboxes: true }) ); return { sciChartSurface, wasmContext }; diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarRadarChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarRadarChart/drawExample.ts index 83c37ba1d..55fba91e1 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarRadarChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarRadarChart/drawExample.ts @@ -5,9 +5,9 @@ import { XyDataSeries, PolarNumericAxis, SciChartPolarSurface, - EColor, - EPolarAxisMode, - EPolarGridlineMode, + EColor, + EPolarAxisMode, + EPolarGridlineMode, PolarCategoryAxis, ENumericFormat, EPolarLabelMode, @@ -19,33 +19,26 @@ import { } from "scichart"; import { appTheme } from "../../../theme"; -const LABELS = [ - "Complexity", - "Memory Usage", - "Stability", - "Adaptability", - "Scalability", - "Cache Efficiency" -]; +const LABELS = ["Complexity", "Memory Usage", "Stability", "Adaptability", "Scalability", "Cache Efficiency"]; const DATA_SET = [ { name: "Quick Sort", color: appTheme.VividSkyBlue, - values: [7, 8, 2, 8, 9, 9] + values: [7, 8, 2, 8, 9, 9], }, - { + { name: "Bubble Sort", color: appTheme.VividOrange, - values: [2, 9, 10, 5, 1, 2], + values: [2, 9, 10, 5, 1, 2], }, -] +]; // this chart expresses the complexity, memory usage, stability, adaptability, scalability, and cache efficiency of two sorting algorithms export const drawExample = async (rootElement: string | HTMLDivElement) => { const { sciChartSurface, wasmContext } = await SciChartPolarSurface.create(rootElement, { - theme: appTheme.SciChartJsTheme + theme: appTheme.SciChartJsTheme, }); const radialYAxis = new PolarNumericAxis(wasmContext, { @@ -58,16 +51,16 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { majorGridLineStyle: { color: EColor.BackgroundColor, strokeThickness: 1, - strokeDashArray: [5, 5] + strokeDashArray: [5, 5], }, drawLabels: false, drawMinorGridLines: false, drawMajorTickLines: false, drawMinorTickLines: false, startAngle: Math.PI / 2, // start at 12 o'clock - innerRadius: 0, + innerRadius: 0, }); - sciChartSurface.yAxes.add(radialYAxis); + sciChartSurface.yAxes.add(radialYAxis); const angularXAxis = new PolarCategoryAxis(wasmContext, { polarAxisMode: EPolarAxisMode.Angular, @@ -75,7 +68,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { majorGridLineStyle: { color: EColor.BackgroundColor, strokeThickness: 1, - strokeDashArray: [5, 5] + strokeDashArray: [5, 5], }, flippedCoordinates: true, // go clockwise drawMinorGridLines: false, @@ -86,19 +79,19 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { }); sciChartSurface.xAxes.add(angularXAxis); - const xValues = Array.from({ length: LABELS.length + 1 }, (_, i) => i); + const xValues = Array.from({ length: LABELS.length + 1 }, (_, i) => i); // +1 to complete the radar chart without overlap of first and last labels - + const polarMountain = new PolarMountainRenderableSeries(wasmContext, { dataSeries: new XyDataSeries(wasmContext, { xValues: xValues, yValues: [...DATA_SET[0].values, DATA_SET[0].values[0]], // +1 append first value to complete the radar chart - dataSeriesName: DATA_SET[0].name + dataSeriesName: DATA_SET[0].name, }), stroke: DATA_SET[0].color, fill: DATA_SET[0].color + "30", strokeThickness: 4, - animation: new FadeAnimation({ duration: 1000 }) + animation: new FadeAnimation({ duration: 1000 }), }); sciChartSurface.renderableSeries.add(polarMountain); @@ -107,7 +100,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { dataSeries: new XyDataSeries(wasmContext, { xValues: xValues, yValues: [...DATA_SET[1].values, DATA_SET[1].values[0]], // +1 append first value to complete the radar chart - dataSeriesName: DATA_SET[1].name + dataSeriesName: DATA_SET[1].name, }), stroke: DATA_SET[1].color, strokeThickness: 4, @@ -118,7 +111,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { fill: DATA_SET[1].color, stroke: EColor.White, }), - animation: new FadeAnimation({ duration: 1000 }) + animation: new FadeAnimation({ duration: 1000 }), }); sciChartSurface.renderableSeries.add(polarLine); @@ -126,7 +119,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { new PolarPanModifier(), new PolarZoomExtentsModifier(), new PolarMouseWheelZoomModifier({ growFactor: 0.0002 }), - new PolarLegendModifier({ showSeriesMarkers: true, showCheckboxes: true }), + new PolarLegendModifier({ showSeriesMarkers: true, showCheckboxes: true }) ); return { sciChartSurface, wasmContext }; diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarRangeColumnChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarRangeColumnChart/drawExample.ts index 72cb0d4ba..ef75faffc 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarRangeColumnChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarRangeColumnChart/drawExample.ts @@ -60,7 +60,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { // replace minors with majors by not drawing majors and setting this: minorsPerMajor: 2, - + drawMajorGridLines: false, drawMinorGridLines: true, drawMajorTickLines: false, diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarScatterChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarScatterChart/drawExample.ts index 4ccb74afb..aa3f770e1 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarScatterChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarScatterChart/drawExample.ts @@ -5,9 +5,9 @@ import { XyDataSeries, PolarNumericAxis, SciChartPolarSurface, - EPolarAxisMode, - NumberRange, - EAxisAlignment, + EPolarAxisMode, + NumberRange, + EAxisAlignment, EPolarLabelMode, PolarXyScatterRenderableSeries, SweepAnimation, @@ -29,11 +29,11 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { axisAlignment: EAxisAlignment.Right, visibleRange: new NumberRange(0, 1400), zoomExtentsToInitialRange: true, - + drawMinorTickLines: false, drawMajorTickLines: false, drawMinorGridLines: false, - + startAngle: Math.PI / 2, drawLabels: false, // no radial labels }); @@ -67,15 +67,15 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { yVals: xValues.map((x) => 2 * x + x * Math.random() * 0.5), color: appTheme.VividOrange, name: "Circle Series", - pointMarkerType: EPointMarkerType.Ellipse - }, + pointMarkerType: EPointMarkerType.Ellipse, + }, { yVals: xValues.map((x) => x + x * Math.random() * 0.5), color: appTheme.VividSkyBlue, name: "Triangular Series", pointMarkerType: EPointMarkerType.Triangle, - } - ] + }, + ]; SCATTER_DATA.forEach(({ yVals, color, name, pointMarkerType }) => { const polarScatter = new PolarXyScatterRenderableSeries(wasmContext, { @@ -96,7 +96,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { stroke: color, strokeThickness: 1, fill: color + "88", - } + }, }, animation: new SweepAnimation({ duration: 800 }), }); @@ -107,7 +107,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { const customMarkerLegendModifier = new PolarLegendModifier({ showCheckboxes: true, showSeriesMarkers: true, - backgroundColor: "#66666633" + backgroundColor: "#66666633", }); // override "getLegendItemHTML" to add custom SVG shapes customMarkerLegendModifier.sciChartLegend.getLegendItemHTML = ( @@ -118,12 +118,12 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { ): string => { const display = orientation === ELegendOrientation.Vertical ? "flex" : "inline-flex"; let str = ``; - + if (showCheckboxes) { const checked = item.checked ? "checked" : ""; str += ``; } - + if (showSeriesMarkers) { str += ` { case SCATTER_DATA[0].name: // Circle return ``; - case SCATTER_DATA[1].name: // Triangle - return ``; + case SCATTER_DATA[1].name: // Triangle + return ``; - default: // Others - return ``; + default: // Others + return ``; } })()} - ` + `; } str += ``; str += ``; @@ -156,9 +160,9 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { new PolarPanModifier(), new PolarZoomExtentsModifier(), new PolarMouseWheelZoomModifier({ - defaultActionType: EActionType.Zoom - }), + defaultActionType: EActionType.Zoom, + }) ); return { sciChartSurface, wasmContext }; -}; \ No newline at end of file +}; diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarStackedMountainChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarStackedMountainChart/drawExample.ts index 77fa119e7..93d69ed3b 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarStackedMountainChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarStackedMountainChart/drawExample.ts @@ -5,9 +5,9 @@ import { XyDataSeries, PolarNumericAxis, SciChartPolarSurface, - EPolarAxisMode, - NumberRange, - EAxisAlignment, + EPolarAxisMode, + NumberRange, + EAxisAlignment, EPolarLabelMode, WaveAnimation, PolarStackedMountainCollection, @@ -65,11 +65,11 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { visibleRange: new NumberRange(0, 10), polarLabelMode: EPolarLabelMode.Parallel, - + startAngle: Math.PI / 2, // start at 12 o'clock flippedCoordinates: true, // go clockwise zoomExtentsToInitialRange: true, - + useNativeText: true, labelPrecision: 0, }); @@ -78,20 +78,19 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { // Make collection to hold all the stacked mountains renderable series const mountainCollection = new PolarStackedMountainCollection(wasmContext); mountainCollection.separatePositiveNegativeStacks = false; - mountainCollection.animation = new WaveAnimation({ duration: 800, zeroLine: 0 }), - - MountainsDatasets.forEach(({yValues, fillColor}) => { - const polarMountain = new PolarStackedMountainRenderableSeries(wasmContext, { - dataSeries: new XyDataSeries(wasmContext, { - xValues: [...xValues, xValues[xValues.length - 1] + 1], // add 1 more xValue to close the loop - yValues: [...yValues, yValues[0]] // close the loop by drawing to the first yValue - }), - fill: fillColor + "BB", // 75% opacity - // stroke: "white", - strokeThickness: 1, + (mountainCollection.animation = new WaveAnimation({ duration: 800, zeroLine: 0 })), + MountainsDatasets.forEach(({ yValues, fillColor }) => { + const polarMountain = new PolarStackedMountainRenderableSeries(wasmContext, { + dataSeries: new XyDataSeries(wasmContext, { + xValues: [...xValues, xValues[xValues.length - 1] + 1], // add 1 more xValue to close the loop + yValues: [...yValues, yValues[0]], // close the loop by drawing to the first yValue + }), + fill: fillColor + "BB", // 75% opacity + // stroke: "white", + strokeThickness: 1, + }); + mountainCollection.add(polarMountain); }); - mountainCollection.add(polarMountain); - }) sciChartSurface.renderableSeries.add(mountainCollection); sciChartSurface.chartModifiers.add( @@ -105,4 +104,4 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { ); return { sciChartSurface, wasmContext }; -}; \ No newline at end of file +}; diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarStackedRadialColumnChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarStackedRadialColumnChart/drawExample.ts index 9994bfacb..22ddc82ea 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarStackedRadialColumnChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarStackedRadialColumnChart/drawExample.ts @@ -5,9 +5,9 @@ import { XyDataSeries, PolarNumericAxis, SciChartPolarSurface, - EPolarAxisMode, - NumberRange, - EAxisAlignment, + EPolarAxisMode, + NumberRange, + EAxisAlignment, EXyDirection, PolarCategoryAxis, TextLabelProvider, @@ -18,22 +18,22 @@ import { ELegendPlacement, GradientParams, Point, - WaveAnimation + WaveAnimation, } from "scichart"; import { appTheme } from "../../../theme"; const DATA: Record = { - "Norway": [122, 125, 111], - "USA": [105, 110, 88], - "Germany": [92, 88, 60], - "Canada": [73, 64, 62], - "Austria": [64, 81, 87], - "Sweden": [57, 46, 55], - "Switzerland": [56, 45, 52], - "Russia": [47, 38, 35], - "Netherlands": [45, 44, 41], - "Finland": [43, 55, 59] -} + Norway: [122, 125, 111], + USA: [105, 110, 88], + Germany: [92, 88, 60], + Canada: [73, 64, 62], + Austria: [64, 81, 87], + Sweden: [57, 46, 55], + Switzerland: [56, 45, 52], + Russia: [47, 38, 35], + Netherlands: [45, 44, 41], + Finland: [43, 55, 59], +}; const COUNTRIES = Object.keys(DATA); const MEDALS = [ @@ -48,7 +48,7 @@ const MEDALS = [ { type: "Bronze", color: appTheme.MutedRed, - } + }, ]; export const drawExample = async (rootElement: string | HTMLDivElement) => { @@ -57,7 +57,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { title: "Winter Olympic medals per country", titleStyle: { fontSize: 24, - } + }, }); // Create Polar, Radial axes @@ -96,20 +96,20 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { autoTicks: false, majorDelta: 25, startAngle: Math.PI, - totalAngle: Math.PI * 3 / 2 // 270 degrees, 3/4 of the circle + totalAngle: (Math.PI * 3) / 2, // 270 degrees, 3/4 of the circle }); sciChartSurface.yAxes.add(yAxis); // SERIES const collection = new PolarStackedColumnCollection(wasmContext); collection.animation = new WaveAnimation({ duration: 1000, fadeEffect: true }); - + const xValues = Array.from({ length: COUNTRIES.length }, (_, i) => i); - for(let i = 0; i < 3; i++){ + for (let i = 0; i < 3; i++) { const polarColumn = new PolarStackedColumnRenderableSeries(wasmContext, { dataSeries: new XyDataSeries(wasmContext, { xValues, - yValues: COUNTRIES.map(country => DATA[country][i]), + yValues: COUNTRIES.map((country) => DATA[country][i]), dataSeriesName: MEDALS[i].type, }), // stroke: "white", @@ -130,7 +130,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { new PolarPanModifier({ xyDirection: EXyDirection.XyDirection, zoomSize: true, - growFactor: 1 + growFactor: 1, }), new PolarZoomExtentsModifier(), new PolarMouseWheelZoomModifier(), @@ -142,4 +142,4 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { ); return { sciChartSurface, wasmContext }; -}; \ No newline at end of file +}; diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarUniformHeatmapChart/README.md b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarUniformHeatmapChart/README.md index b46b5fa8a..e593c5dc0 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarUniformHeatmapChart/README.md +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarUniformHeatmapChart/README.md @@ -6,85 +6,94 @@ This example demonstrates a **Polar Uniform Heatmap Chart** using SciChart.js, v ## Technologies Used -- SciChart.js – High performance WebGL charting library -- WebAssembly – For accelerated computation -- TypeScript – Used for type safety -- Heatmap visualization – For intensity data representation +- SciChart.js – High performance WebGL charting library +- WebAssembly – For accelerated computation +- TypeScript – Used for type safety +- Heatmap visualization – For intensity data representation ## Code Explanation The example centers around two main functions: `drawExample` for the polar heatmap chart and `drawHeatmapLegend` for the accompanying legend. Key components include: -1. **Polar Surface Setup**: - - Created using `SciChartPolarSurface.create()` with custom padding - - Configured with two axes: - - `PolarNumericAxis` in `EPolarAxisMode.Radial` mode for the radial dimension - - `PolarNumericAxis` in `EPolarAxisMode.Angular` mode for the angular dimension +1. **Polar Surface Setup**: + + - Created using `SciChartPolarSurface.create()` with custom padding + - Configured with two axes: + - `PolarNumericAxis` in `EPolarAxisMode.Radial` mode for the radial dimension + - `PolarNumericAxis` in `EPolarAxisMode.Angular` mode for the angular dimension 2. **Heatmap Series**: - - Uses `PolarUniformHeatmapRenderableSeries` with a `UniformHeatmapDataSeries` - - Data generated via `generateHeatmapData()` producing a 300x500 array of normalized values - - Custom `HeatmapColorMap` with vivid gradient stops defines the color representation + + - Uses `PolarUniformHeatmapRenderableSeries` with a `UniformHeatmapDataSeries` + - Data generated via `generateHeatmapData()` producing a 300x500 array of normalized values + - Custom `HeatmapColorMap` with vivid gradient stops defines the color representation 3. **Interactive Modifiers**: - - `PolarPanModifier` for panning - - `PolarZoomExtentsModifier` for zoom-to-fit - - `PolarMouseWheelZoomModifier` for zooming with mouse wheel + + - `PolarPanModifier` for panning + - `PolarZoomExtentsModifier` for zoom-to-fit + - `PolarMouseWheelZoomModifier` for zooming with mouse wheel 4. **Data Generation**: - - Complex algorithm combining: - - Random seeded features with radial influence - - Wave patterns with periodic functions - - Smoothing via neighborhood averaging - - Value polarization using hyperbolic tangent + - Complex algorithm combining: + - Random seeded features with radial influence + - Wave patterns with periodic functions + - Smoothing via neighborhood averaging + - Value polarization using hyperbolic tangent ## Customization Key customizations in this example include: 1. **Polar Layout Configuration**: - - `innerRadius: 0.2` creates a donut-shaped heatmap - - `totalAngle: Math.PI / 2` limits the chart to 90 degrees - - `flippedCoordinates: true` reverses angular direction + + - `innerRadius: 0.2` creates a donut-shaped heatmap + - `totalAngle: Math.PI / 2` limits the chart to 90 degrees + - `flippedCoordinates: true` reverses angular direction 2. **Advanced Data Processing**: - - The `generateHeatmapData` function implements: - - Seeded pseudo-random generation for reproducible patterns - - Multiple influence layers (features + waves) - - Smoothing pass with 3x3 neighborhood averaging - - Value polarization using `Math.tanh` for enhanced contrast + + - The `generateHeatmapData` function implements: + - Seeded pseudo-random generation for reproducible patterns + - Multiple influence layers (features + waves) + - Smoothing pass with 3x3 neighborhood averaging + - Value polarization using `Math.tanh` for enhanced contrast 3. **Shared Color Mapping**: - - The `COLOR_MAP` instance is shared between chart and legend - - Custom gradient stops create a vivid spectrum from pink to dark indigo + + - The `COLOR_MAP` instance is shared between chart and legend + - Custom gradient stops create a vivid spectrum from pink to dark indigo 4. **Legend Styling**: - - Transparent dark indigo background - - Custom tick and label styling matching the theme - - Right-aligned with inner axis placement + - Transparent dark indigo background + - Custom tick and label styling matching the theme + - Right-aligned with inner axis placement ## Running the Example To run this example from the SciChart.JS.Examples repository: 1. **Clone the Repository**: - ```bash - git clone https://github.com/ABTSoftware/SciChart.JS.Examples.git - ``` + + ```bash + git clone https://github.com/ABTSoftware/SciChart.JS.Examples.git + ``` 2. **Navigate to the Examples Directory**: - ```bash - cd SciChart.JS.Examples/Examples - ``` + + ```bash + cd SciChart.JS.Examples/Examples + ``` 3. **Install Dependencies**: - ```bash - npm install - ``` + + ```bash + npm install + ``` 4. **Run the Development Server**: - ```bash - npm run dev - ``` + ```bash + npm run dev + ``` -For more details, refer to the [SciChart.JS.Examples README](https://github.com/ABTSoftware/SciChart.JS.Examples/blob/master/README.md) and [Polar Heatmap Documentation](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/polar-uniform-heatmap-renderable-series). \ No newline at end of file +For more details, refer to the [SciChart.JS.Examples README](https://github.com/ABTSoftware/SciChart.JS.Examples/blob/master/README.md) and [Polar Heatmap Documentation](https://www.scichart.com/documentation/js/v5/2d-charts/chart-types/polar-uniform-heatmap-renderable-series). diff --git a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarWindroseColumnChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarWindroseColumnChart/drawExample.ts index 716d94c74..c1a1b0dd5 100644 --- a/Examples/src/components/Examples/Charts2D/PolarCharts/PolarWindroseColumnChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/PolarCharts/PolarWindroseColumnChart/drawExample.ts @@ -5,15 +5,15 @@ import { XyDataSeries, PolarNumericAxis, SciChartPolarSurface, - EPolarAxisMode, - NumberRange, - EAxisAlignment, + EPolarAxisMode, + NumberRange, + EAxisAlignment, PolarStackedColumnCollection, PolarStackedColumnRenderableSeries, TFormatLabelFn, NumericLabelProvider, EDataPointWidthMode, - WaveAnimation + WaveAnimation, } from "scichart"; import { appTheme } from "../../../theme"; @@ -36,7 +36,7 @@ function getBiasedRandomWalkInBounds(min: number, max: number, count: number) { } /** - * Custom label provider that displays compass directions at 45 degree intervals, + * Custom label provider that displays compass directions at 45 degree intervals, * if any label value is NOT from `[0, 45, 90, 135, 180, 225, 270, 315]` it will be a decimal. */ class CustomNESWLabelProvider extends NumericLabelProvider { @@ -64,7 +64,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { polarAxisMode: EPolarAxisMode.Radial, visibleRange: new NumberRange(0, 6), zoomExtentsToInitialRange: true, - + drawMinorGridLines: false, drawMajorTickLines: false, drawMinorTickLines: false, @@ -73,7 +73,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { autoTicks: false, majorDelta: 1, labelPrecision: 0, - innerRadius: 0.05 // center hole + innerRadius: 0.05, // center hole }); sciChartSurface.yAxes.add(radialYAxis); @@ -88,11 +88,11 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { autoTicks: false, majorDelta: 15, drawMinorGridLines: false, - zoomExtentsToInitialRange: true + zoomExtentsToInitialRange: true, }); sciChartSurface.xAxes.add(polarXAxis); - const xValues = Array.from({length: COLUMN_COUNT}, (_, i) => i * 360 / COLUMN_COUNT); // [0, 10, ..., 350], + const xValues = Array.from({ length: COLUMN_COUNT }, (_, i) => (i * 360) / COLUMN_COUNT); // [0, 10, ..., 350], const yValues = [ getBiasedRandomWalkInBounds(1, 2, COLUMN_COUNT), getBiasedRandomWalkInBounds(0.3, 1, COLUMN_COUNT), @@ -107,13 +107,13 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { appTheme.VividGreen, appTheme.VividOrange, appTheme.VividPink, - ] + ]; const collection = new PolarStackedColumnCollection(wasmContext, { isOneHundredPercent: false, }); - - for(let i = 0; i < yValues.length; i++) { + + for (let i = 0; i < yValues.length; i++) { const dataSeries = new XyDataSeries(wasmContext, { xValues, yValues: yValues[i] }); const polarColumn = new PolarStackedColumnRenderableSeries(wasmContext, { dataSeries, diff --git a/Examples/src/components/Examples/Charts2D/TooltipsAndHittest/HitTestAPI/index.tsx b/Examples/src/components/Examples/Charts2D/TooltipsAndHittest/HitTestAPI/index.tsx index 10a15b15e..67896c301 100644 --- a/Examples/src/components/Examples/Charts2D/TooltipsAndHittest/HitTestAPI/index.tsx +++ b/Examples/src/components/Examples/Charts2D/TooltipsAndHittest/HitTestAPI/index.tsx @@ -30,15 +30,9 @@ export default function ChartComponent() { color="primary" aria-label="small outlined button group" > - - Hit-Test Datapoint - - - Hit-Test X-Slice - - - Hit-Test Series Body - + Hit-Test Datapoint + Hit-Test X-Slice + Hit-Test Series Body - + ${rows} `; }; diff --git a/Examples/src/components/Examples/Charts2D/ZoomingAndPanning/PolarModifiers/drawExample.ts b/Examples/src/components/Examples/Charts2D/ZoomingAndPanning/PolarModifiers/drawExample.ts index f0969443b..0d6ac992b 100644 --- a/Examples/src/components/Examples/Charts2D/ZoomingAndPanning/PolarModifiers/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/ZoomingAndPanning/PolarModifiers/drawExample.ts @@ -30,7 +30,8 @@ import { import { appTheme } from "../../../theme"; export const POLAR_MODIFIER_INFO: Partial> = { - [EChart2DModifierType.PolarZoomExtents]: "Double-click\nto reset the zoom at the original visible ranges.\n(pairs amazing with other modifiers)", + [EChart2DModifierType.PolarZoomExtents]: + "Double-click\nto reset the zoom at the original visible ranges.\n(pairs amazing with other modifiers)", [EChart2DModifierType.PolarMouseWheelZoom]: "Zoom The Polar Chart\nusing the mouse wheel or touchpad", [EChart2DModifierType.PolarMouseWheelZoom + " [Pan]"]: "Rotate The Polar Chart\nusing the mouse wheel or touchpad", [EChart2DModifierType.PolarPan + " [Cartesian]"]: "Click and drag\nto pan the chart in Cartesian mode", @@ -100,7 +101,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { paletteProvider: new DataPointSelectionPaletteProvider({ fill: "#FFFFFF", stroke: "#00AA00", - }) + }), }); sciChartSurface.renderableSeries.add(polarColumn); @@ -115,7 +116,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { horizontalAnchorPoint: EHorizontalAnchorPoint.Center, multiLineAlignment: EMultiLineAlignment.Center, lineSpacing: 5, - textColor: appTheme.TextColor + textColor: appTheme.TextColor, }); sciChartSurface.annotations.add(detailTextAnnotation); @@ -144,11 +145,11 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { backgroundColor: appTheme.DarkIndigo, textColor: STROKE, }); - const PolarMouseWheelZoom = new PolarMouseWheelZoomModifier({ - defaultActionType: EActionType.Zoom + const PolarMouseWheelZoom = new PolarMouseWheelZoomModifier({ + defaultActionType: EActionType.Zoom, }); - const PolarMouseWheelZoomPAN = new PolarMouseWheelZoomModifier({ - defaultActionType: EActionType.Pan + const PolarMouseWheelZoomPAN = new PolarMouseWheelZoomModifier({ + defaultActionType: EActionType.Pan, }); const PolarPanCartesian = new PolarPanModifier({ primaryPanMode: EPolarPanModifierPanMode.Cartesian, @@ -185,7 +186,7 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { return PolarPanCartesian; case EChart2DModifierType.PolarPan + " [Polar]": return PolarPanPolar; - + case EChart2DModifierType.PolarZoomExtents: return PolarZoomExtents; default: diff --git a/Examples/src/components/Examples/Charts2D/ZoomingAndPanning/PolarModifiers/index.tsx b/Examples/src/components/Examples/Charts2D/ZoomingAndPanning/PolarModifiers/index.tsx index 865782dff..4357ceaca 100644 --- a/Examples/src/components/Examples/Charts2D/ZoomingAndPanning/PolarModifiers/index.tsx +++ b/Examples/src/components/Examples/Charts2D/ZoomingAndPanning/PolarModifiers/index.tsx @@ -9,34 +9,24 @@ import { appTheme } from "../../../theme"; const ALL_POLAR_MODIFIER_TYPES = Array.from(Object.keys(POLAR_MODIFIER_INFO)); const CONFLICTING_MODIFIER_TYPES = [ - [ - EChart2DModifierType.PolarPan, - EChart2DModifierType.PolarArcZoom, - ], - [ - EChart2DModifierType.PolarMouseWheelZoom, - EChart2DModifierType.PolarMouseWheelZoom + " [Pan]" - ], - [ - EChart2DModifierType.PolarPan + " [Cartesian]", - EChart2DModifierType.PolarPan + " [Polar]", - ] + [EChart2DModifierType.PolarPan, EChart2DModifierType.PolarArcZoom], + [EChart2DModifierType.PolarMouseWheelZoom, EChart2DModifierType.PolarMouseWheelZoom + " [Pan]"], + [EChart2DModifierType.PolarPan + " [Cartesian]", EChart2DModifierType.PolarPan + " [Polar]"], ]; - // React component needed as our examples app is react. // SciChart can be used in Angular, Vue, Blazor and vanilla JS! See our Github repo for more info export default function ChartComponent() { - const [ modifiersActive, setModifiersActive ] = useState<{ [key: string]: boolean }>({ + const [modifiersActive, setModifiersActive] = useState<{ [key: string]: boolean }>({ [EChart2DModifierType.PolarZoomExtents]: true, [EChart2DModifierType.PolarMouseWheelZoom]: true, - [EChart2DModifierType.PolarPan + " [Cartesian]"]: true + [EChart2DModifierType.PolarPan + " [Cartesian]"]: true, }); - const [ conflictWarning, setConflictWarning ] = useState(null); + const [conflictWarning, setConflictWarning] = useState(null); - const [controls, setControls] = useState({ + const [controls, setControls] = useState({ toggleModifier: (modifier: EChart2DModifierType) => {}, - }) + }); const handleToggleButtonChanged = (e: any, value: EChart2DModifierType) => { if (value === null) return; @@ -51,7 +41,7 @@ export default function ChartComponent() { let hasConflict = false; let conflictMessage = ""; - + for (const pair of CONFLICTING_MODIFIER_TYPES) { if (newState[pair[0]] && newState[pair[1]]) { hasConflict = true; @@ -59,7 +49,7 @@ export default function ChartComponent() { break; } } - + if (hasConflict) { setConflictWarning(conflictMessage); } else { @@ -72,58 +62,72 @@ export default function ChartComponent() { return (
-
-
+
+

Polar Modifiers:

{Object.values(ALL_POLAR_MODIFIER_TYPES).map((type) => ( -
+
handleToggleButtonChanged(event, type as EChart2DModifierType)} - inputProps={{ 'aria-label': 'controlled' }} + inputProps={{ "aria-label": "controlled" }} style={{ color: appTheme.Indigo, }} /> -

{type}

+

+ {type} +

))} {/* conflict handling */} {conflictWarning && ( -
+
{conflictWarning}
)} @@ -137,5 +141,5 @@ export default function ChartComponent() { />
- ) + ); } diff --git a/Examples/src/components/Examples/Charts2D/v4Charts/AnimatedColumns/README.md b/Examples/src/components/Examples/Charts2D/v4Charts/AnimatedColumns/README.md index b78bcf84d..baec540ae 100644 --- a/Examples/src/components/Examples/Charts2D/v4Charts/AnimatedColumns/README.md +++ b/Examples/src/components/Examples/Charts2D/v4Charts/AnimatedColumns/README.md @@ -6,10 +6,10 @@ This example demonstrates how to create an animated column chart visualizing ATP ## Technologies Used -- SciChart.js – High performance WebGL charting library -- Vanilla JavaScript – Core implementation -- TypeScript – Type definitions -- WebGL – For GPU-accelerated rendering +- SciChart.js – High performance WebGL charting library +- Vanilla JavaScript – Core implementation +- TypeScript – Type definitions +- WebGL – For GPU-accelerated rendering ## Code Explanation @@ -36,16 +36,19 @@ Key non-obvious customizations in this example: To run this example from the SciChart.JS.Examples repository: 1. **Clone the Repository**: + ```bash git clone https://github.com/ABTSoftware/SciChart.JS.Examples.git ``` 2. **Navigate to the Example**: + ```bash cd SciChart.JS.Examples/Examples/src/components/Charts2D/v4Charts/AnimatedColumns ``` 3. **Install Dependencies**: + ```bash npm install ``` @@ -55,4 +58,4 @@ To run this example from the SciChart.JS.Examples repository: npm run dev ``` -For framework-specific implementations (React, Angular), refer to the corresponding files in the example directory. \ No newline at end of file +For framework-specific implementations (React, Angular), refer to the corresponding files in the example directory. diff --git a/Examples/src/components/Examples/Charts2D/v4Charts/BoxPlotChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/v4Charts/BoxPlotChart/drawExample.ts index 7f2282bad..d5f1b88d2 100644 --- a/Examples/src/components/Examples/Charts2D/v4Charts/BoxPlotChart/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/v4Charts/BoxPlotChart/drawExample.ts @@ -131,11 +131,10 @@ export const drawExample = async (rootElement: string | HTMLDivElement) => { }, }); sub2.renderableSeries.add(boxSeries2); - - sub2.chartModifiers.add( - new ZoomPanModifier(), - new ZoomExtentsModifier() - ); + + sub2.chartModifiers.add(new ZoomPanModifier(), new ZoomExtentsModifier()); + + sub2.chartModifiers.add(new ZoomPanModifier(), new ZoomExtentsModifier()); return { sciChartSurface, wasmContext }; }; diff --git a/Examples/src/components/Examples/Charts2D/v4Charts/LinearGauges/exampleInfo.tsx b/Examples/src/components/Examples/Charts2D/v4Charts/LinearGauges/exampleInfo.tsx index 476d7aba3..f43eb7b61 100644 --- a/Examples/src/components/Examples/Charts2D/v4Charts/LinearGauges/exampleInfo.tsx +++ b/Examples/src/components/Examples/Charts2D/v4Charts/LinearGauges/exampleInfo.tsx @@ -19,7 +19,7 @@ const metaData: IExampleMetadata = metaDescription: "View the JavaScript Linear Gauge Chart example to combine rectangles & annotations. Create a linear gauge dashboard with animated indicators and custom scales.", markdownContent: - "## Linear Gauges - JavaScript\n\n### Overview\nThis example demonstrates **linear gauge** visualization using SciChart.js, featuring multiple gauge styles with [ArrowAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/linearrowannotation.html) markers and [TextAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/textannotation.html) for non-linear labels. The implementation shows how to create professional dashboard gauges with custom scales.\n\n### Technical Implementation\nThe gauges use [FastRectangleRenderableSeries](https://www.scichart.com/documentation/js/v5/typedoc/classes/fastbandrenderableseries.html) with [XyxyDataSeries](https://www.scichart.com/documentation/js/v5/typedoc/classes/xyxydataseries.html) for rectangular segments. Value indicators are implemented via [LineArrowAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/linearrowannotation.html). For advanced scale customization, the example could be extended by overriding [NumericTickProvider](https://www.scichart.com/documentation/js/v5/typedoc/classes/numerictickprovider.html) on the axis doing \`xAxes.tickProvider = new NumericTickProvider(wasmContext)\`.\n\n### Features and Capabilities\nKey features include vertical/horizontal gauge orientations, gradient fills, and dynamic value indicators. The implementation shows how to use [ECoordinateMode](https://www.scichart.com/documentation/js/v5/typedoc/enums/ecoordinatemode.html) for precise annotation positioning and [IFillPaletteProvider](https://www.scichart.com/documentation/js/v5/typedoc/interfaces/ifillpaletteprovider.html) for segmented coloring.\n\n### Integration and Best Practices\nThe vanilla JS implementation follows best practices for async initialization with proper cleanup. For production use, consider implementing custom [IAxisTickProvider](https://www.scichart.com/documentation/js/v5/typedoc/interfaces/iaxistickprovider.html) for non-linear gauge scales.", + "## Linear Gauges - JavaScript\n\n### Overview\nThis example demonstrates **linear gauge** visualization using SciChart.js, featuring multiple gauge styles with [ArrowAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/linearrowannotation.html) markers and [TextAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/textannotation.html) for non-linear labels. The implementation shows how to create professional dashboard gauges with custom scales.\n\n### Technical Implementation\nThe gauges use [FastRectangleRenderableSeries](https://www.scichart.com/documentation/js/v5/typedoc/classes/fastbandrenderableseries.html) with [XyxyDataSeries](https://www.scichart.com/documentation/js/v5/typedoc/classes/xyxydataseries.html) for rectangular segments. Value indicators are implemented via [LineArrowAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/linearrowannotation.html). For advanced scale customization, the example could be extended by overriding [NumericTickProvider](https://www.scichart.com/documentation/js/v5/typedoc/classes/numerictickprovider.html) on the axis doing `xAxes.tickProvider = new NumericTickProvider(wasmContext)`.\n\n### Features and Capabilities\nKey features include vertical/horizontal gauge orientations, gradient fills, and dynamic value indicators. The implementation shows how to use [ECoordinateMode](https://www.scichart.com/documentation/js/v5/typedoc/enums/ecoordinatemode.html) for precise annotation positioning and [IFillPaletteProvider](https://www.scichart.com/documentation/js/v5/typedoc/interfaces/ifillpaletteprovider.html) for segmented coloring.\n\n### Integration and Best Practices\nThe vanilla JS implementation follows best practices for async initialization with proper cleanup. For production use, consider implementing custom [IAxisTickProvider](https://www.scichart.com/documentation/js/v5/typedoc/interfaces/iaxistickprovider.html) for non-linear gauge scales.", }, react: { subtitle: @@ -29,7 +29,7 @@ const metaData: IExampleMetadata = metaDescription: "View the React Linear Gauge Chart example to combine rectangles & annotations. Create a linear gauge dashboard with animated indicators and custom scales.", markdownContent: - "## Linear Gauges - React\n\n### Overview\nThis React example showcases **dashboard-style linear gauges** using the [SciChartReact](https://www.scichart.com/blog/react-charts-with-scichart-js/) component. It demonstrates React integration patterns for gauge components with animated indicators and custom scales.\n\n### Technical Implementation\nThe implementation uses [FastRectangleRenderableSeries](https://www.scichart.com/documentation/js/v5/typedoc/classes/fastbandrenderableseries.html) for gauge bodies and [LineArrowAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/linearrowannotation.html) for value pointers. React hooks manage gauge state, while [TextAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/textannotation.html) handles custom labels. For advanced implementations, consider creating a custom component [NumericTickProvider](https://www.scichart.com/documentation/js/v5/typedoc/classes/numerictickprovider.html) on the axis doing \`xAxes.tickProvider = new NumericTickProvider(wasmContext)\`.\n\n### Features and Capabilities\nThe example includes responsive gauges with both vertical and horizontal layouts. It demonstrates React state management for gauge values and uses [useEffect](https://react.dev/reference/react/useEffect) for clean animation handling. The [ECoordinateMode](https://www.scichart.com/documentation/js/v5/typedoc/enums/ecoordinatemode.html) API ensures precise label positioning.\n\n### Integration and Best Practices\nFollow the [SciChart React Tutorial](https://www.scichart.com/documentation/js/v5/get-started/tutorials-react/tutorial-01-setting-up-project-with-scichart-react/) for proper component integration. For production apps, consider memoizing gauge configurations and implementing custom tick providers for non-linear scales.", + "## Linear Gauges - React\n\n### Overview\nThis React example showcases **dashboard-style linear gauges** using the [SciChartReact](https://www.scichart.com/blog/react-charts-with-scichart-js/) component. It demonstrates React integration patterns for gauge components with animated indicators and custom scales.\n\n### Technical Implementation\nThe implementation uses [FastRectangleRenderableSeries](https://www.scichart.com/documentation/js/v5/typedoc/classes/fastbandrenderableseries.html) for gauge bodies and [LineArrowAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/linearrowannotation.html) for value pointers. React hooks manage gauge state, while [TextAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/textannotation.html) handles custom labels. For advanced implementations, consider creating a custom component [NumericTickProvider](https://www.scichart.com/documentation/js/v5/typedoc/classes/numerictickprovider.html) on the axis doing `xAxes.tickProvider = new NumericTickProvider(wasmContext)`.\n\n### Features and Capabilities\nThe example includes responsive gauges with both vertical and horizontal layouts. It demonstrates React state management for gauge values and uses [useEffect](https://react.dev/reference/react/useEffect) for clean animation handling. The [ECoordinateMode](https://www.scichart.com/documentation/js/v5/typedoc/enums/ecoordinatemode.html) API ensures precise label positioning.\n\n### Integration and Best Practices\nFollow the [SciChart React Tutorial](https://www.scichart.com/documentation/js/v5/get-started/tutorials-react/tutorial-01-setting-up-project-with-scichart-react/) for proper component integration. For production apps, consider memoizing gauge configurations and implementing custom tick providers for non-linear scales.", }, angular: { subtitle: @@ -39,7 +39,7 @@ const metaData: IExampleMetadata = metaDescription: "View the Angular Linear Gauge Chart example to combine rectangles & annotations. Create a linear gauge dashboard with animated indicators and custom scales.", markdownContent: - "## Linear Gauges - Angular\n\n### Overview\nThis Angular example demonstrates **enterprise-grade linear gauges** using the [scichart-angular](https://www.npmjs.com/package/scichart-angular) package. It features standalone components with custom scale markers and annotation-based labels.\n\n### Technical Implementation\nThe gauges are implemented through Angular's [standalone component](https://angular.io/guide/standalone-components) architecture. [FastRectangleRenderableSeries](https://www.scichart.com/documentation/js/v5/typedoc/classes/fastbandrenderableseries.html) creates gauge bodies, while [LineArrowAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/linearrowannotation.html) serves as value indicators. For advanced implementations, create a custom [NumericTickProvider](https://www.scichart.com/documentation/js/v5/typedoc/classes/numerictickprovider.html) on the axis doing \`xAxes.tickProvider = new NumericTickProvider(wasmContext)\`.\n\n### Features and Capabilities\nThe example showcases Angular's change detection working with SciChart's rendering pipeline. It includes responsive gauge layouts and demonstrates zone.js compatibility with animations. The [TextAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/textannotation.html) API handles custom scale labels.\n\n### Implement [OnDestroy](https://angular.io/api/core/OnDestroy) for chart cleanup and consider services for shared tick provider logic across multiple gauge components.", + "## Linear Gauges - Angular\n\n### Overview\nThis Angular example demonstrates **enterprise-grade linear gauges** using the [scichart-angular](https://www.npmjs.com/package/scichart-angular) package. It features standalone components with custom scale markers and annotation-based labels.\n\n### Technical Implementation\nThe gauges are implemented through Angular's [standalone component](https://angular.io/guide/standalone-components) architecture. [FastRectangleRenderableSeries](https://www.scichart.com/documentation/js/v5/typedoc/classes/fastbandrenderableseries.html) creates gauge bodies, while [LineArrowAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/linearrowannotation.html) serves as value indicators. For advanced implementations, create a custom [NumericTickProvider](https://www.scichart.com/documentation/js/v5/typedoc/classes/numerictickprovider.html) on the axis doing `xAxes.tickProvider = new NumericTickProvider(wasmContext)`.\n\n### Features and Capabilities\nThe example showcases Angular's change detection working with SciChart's rendering pipeline. It includes responsive gauge layouts and demonstrates zone.js compatibility with animations. The [TextAnnotation](https://www.scichart.com/documentation/js/v5/typedoc/classes/textannotation.html) API handles custom scale labels.\n\n### Implement [OnDestroy](https://angular.io/api/core/OnDestroy) for chart cleanup and consider services for shared tick provider logic across multiple gauge components.", }, }, documentationLinks: [ diff --git a/Examples/src/components/Examples/Charts2D/v4Charts/MapExample/drawExample.ts b/Examples/src/components/Examples/Charts2D/v4Charts/MapExample/drawExample.ts index d1ec1b158..00b2840bf 100644 --- a/Examples/src/components/Examples/Charts2D/v4Charts/MapExample/drawExample.ts +++ b/Examples/src/components/Examples/Charts2D/v4Charts/MapExample/drawExample.ts @@ -20,13 +20,7 @@ import { import { appTheme } from "../../../theme"; -import { - getMinMax, - interpolateColor, - keyData, - australiaData, - preserveAspectRatio, -} from "./helpers"; +import { getMinMax, interpolateColor, keyData, australiaData, preserveAspectRatio } from "./helpers"; import { australianCities } from "./australiaData"; diff --git a/Examples/src/components/Examples/FeaturedApps/PerformanceDemos/Load1MillionPoints/index.tsx b/Examples/src/components/Examples/FeaturedApps/PerformanceDemos/Load1MillionPoints/index.tsx index 3f47da9e3..63e3db7b8 100644 --- a/Examples/src/components/Examples/FeaturedApps/PerformanceDemos/Load1MillionPoints/index.tsx +++ b/Examples/src/components/Examples/FeaturedApps/PerformanceDemos/Load1MillionPoints/index.tsx @@ -32,12 +32,12 @@ export default function Load1MillionPointsChart() { return controls.stopUpdate; }} /> -
+ + +
+ + {/* Examples dropdown menu */} + setExamplesAnchor(null)} + > + {SCENARIOS.map((scenario) => ( + { + loadScenario(scenario); + setExamplesAnchor(null); + }} + style={{ fontSize: 12 }} + > + {scenario.title} + + ))} + + { + dispatch({ type: "CLEAR" }); + setExamplesAnchor(null); + }} + style={{ fontSize: 12 }} + > + Clear + + + + {/* Chain floating panel */} + setChainOpen(false)} + defaultPosition={{ x: 8, y: 110 }} + > +
+ {/* VSWR */} +
+ + + VSWR: + + + { + const v = parseFloat(e.target.value); + if (v > 1) dispatch({ type: "SET_VSWR", vswr: v }); + }} + /> + + + dispatch({ type: "SET_VSWR_SHADED", shaded: e.target.checked }) + } + /> + } + label={Shade} + /> + + + + dispatch({ type: "SET_VSWR_OUTLINE", outline: e.target.checked }) + } + /> + } + label={Outline} + /> + +
+ + + + {/* Chain builder */} +
+ + + Freq: + + + { + const v = parseFloat(e.target.value); + if (v > 0) dispatch({ type: "SET_FREQUENCY", frequency: v * 1e9 }); + }} + /> + GHz + + + + + setChainValue(e.target.value)} + placeholder={chainType === "TL" ? "λ" : "SI"} + /> + + + + + + + + + + + +
+
+
+ + {/* Grid Config floating panel */} + setGridOpen(false)} + defaultPosition={{ x: 280, y: 110 }} + > +
+ {/* Z/Y/ZY grid mode */} +
+ Grid: + v && dispatch({ type: "SET_GRID_MODE", mode: v })} + > + + Z + + + Y + + + ZY + + +
+ + {/* Z opacity */} + {(state.gridMode === "Z" || state.gridMode === "ZY") && ( + +
+ Z α: + + dispatch({ type: "SET_Z_OPACITY", opacity: v as number }) + } + style={{ flex: 1 }} + /> +
+
+ )} + + {/* Y opacity */} + {(state.gridMode === "Y" || state.gridMode === "ZY") && ( + +
+ Y α: + + dispatch({ type: "SET_Y_OPACITY", opacity: v as number }) + } + style={{ flex: 1 }} + /> +
+
+ )} + + + + applyGridCfg({ majorPxThreshold: v })} + /> + applyGridCfg({ minorPxThreshold: v })} + /> + applyGridCfg({ targetTicks: v })} + /> + applyGridCfg({ maxTiers: v })} + /> + applyGridCfg({ minGapPx: v })} + /> + + applyGridCfg({ useCompactRange: e.target.checked })} + /> + } + label={Compact range} + style={{ margin: 0 }} + /> + + + + RIM + + applyRimCfg({ labelOffset: v })} + /> + applyRimCfg({ majorTickStep: v })} + /> + applyRimCfg({ minorTickStep: v })} + /> +
+
+
+ + {/* Readout sidebar */} +
+ {state.scenarioSteps.length > 0 && ( + <> + + HOW IT WORKS + + {state.scenarioSteps.map((step, i) => ( +
+ + {i + 1}. + + + {step} + +
+ ))} + + + )} + + MARKERS + + {state.markers.length === 0 && ( + + Click chart to place a marker + + )} + {state.markers.map((marker, i) => { + const ro = computeReadouts(marker.gamma); + const colour = COLOURS[i % COLOURS.length]; + const isActive = marker.id === state.activeMarkerId; + return ( + + dispatch({ type: "SET_ACTIVE_MARKER", id: isActive ? null : marker.id }) + } + sx={{ + border: `1px solid ${colour}`, + bgcolor: "var(--bg-chart)", + color: "var(--text)", + "&:before": { display: "none" }, + "& .MuiAccordionSummary-root": { bgcolor: "var(--bg-chart)", color: "var(--text)" }, + "& .MuiAccordionDetails-root": { bgcolor: "var(--bg-chart)" }, + "& .MuiToggleButton-root": { + color: "color-mix(in srgb, var(--text) 60%, transparent)", + borderColor: "color-mix(in srgb, var(--text) 25%, transparent)", + }, + "& .MuiToggleButton-root.Mui-selected": { + bgcolor: "color-mix(in srgb, var(--text) 15%, transparent)", + color: "var(--text)", + }, + }} + > + } + style={{ minHeight: 32, padding: "0 8px" }} + > + { + e.stopPropagation(); + dispatch({ type: "REMOVE_MARKER", id: marker.id }); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.stopPropagation(); + dispatch({ type: "REMOVE_MARKER", id: marker.id }); + } + }} + style={{ + marginRight: 4, + padding: 2, + cursor: "pointer", + display: "inline-flex", + alignItems: "center", + borderRadius: "50%", + }} + > + + + + + Γ={marker.gamma.re.toFixed(3)} + {marker.gamma.im >= 0 ? "+" : ""}j{marker.gamma.im.toFixed(3)} + + + +
+ + Drag: + + + v && + dispatch({ + type: "SET_DRAG_MODE", + id: marker.id, + mode: v as DragMode, + }) + } + > + {(["free", "gamma", "R", "X", "G", "B"] as DragMode[]).map((m) => ( + + {m === "free" ? "Free" : m === "gamma" ? "|Γ|" : m} + + ))} + +
+ +
+
+ ); + })} + + {/* Chain step list */} + {state.chain.length > 0 && ( + <> + + + CHAIN ({state.chain.length} steps) + + {state.chain.map((step, i) => ( +
+
+ + {step.type} {step.value.toExponential(2)} + {" → "}Γ={step.toGamma.re.toFixed(3)} + {step.toGamma.im >= 0 ? "+" : ""}j{step.toGamma.im.toFixed(3)} + +
+ ))} + {(state.markers.find((m) => m.isChainStart) || state.chainStartGamma) && ( + + Start:{" "} + {(() => { + const m = state.markers.find((m) => m.isChainStart); + if (m) return m.label; + const g = state.chainStartGamma!; + return `Γ=(${g.re.toFixed(3)},${g.im.toFixed(3)})`; + })()} + + )} + + )} +
+
+
+ ); +} + +function GridSlider({ + label, + tooltip, + min, + max, + step, + value, + onChange, + format, +}: { + label: string; + tooltip?: string; + min: number; + max: number; + step: number; + value: number; + onChange: (v: number) => void; + format?: (v: number) => string; +}) { + const inner = ( +
+ + {label}: + + onChange(v as number)} + style={{ flex: 1 }} + /> + + {format ? format(value) : value} + +
+ ); + return tooltip ? ( + + {inner} + + ) : ( + inner + ); +} + +function ReadoutTable({ ro }: { ro: ReturnType }) { + const rows: [string, string][] = [ + ["|Γ|", ro.gammaMag.toFixed(4)], + ["∠Γ", `${ro.gammaAngleDeg.toFixed(2)}°`], + ["Z", `${ro.zr.toFixed(3)} ${ro.zx >= 0 ? "+" : "−"} j${Math.abs(ro.zx).toFixed(3)}`], + ["Y", `${ro.gy.toFixed(3)} ${ro.by >= 0 ? "+" : "−"} j${Math.abs(ro.by).toFixed(3)}`], + ["VSWR", isFinite(ro.vswr) ? ro.vswr.toFixed(3) : "∞"], + ["RL", isFinite(ro.returnLoss) ? `${ro.returnLoss.toFixed(2)} dB` : "∞"], + ["ML", isFinite(ro.mismatchLoss) ? `${ro.mismatchLoss.toFixed(3)} dB` : "∞"], + ["Q", isFinite(ro.q) ? ro.q.toFixed(3) : "∞"], + ["WTG", ro.wtg.toFixed(4) + " λ"], + ["WTL", ro.wtl.toFixed(4) + " λ"], + ]; + return ( + + + {rows.map(([label, value]) => ( + + + + + ))} + +
+ {label} + {value}
+ ); +} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smith-chart.jpg b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smith-chart.jpg new file mode 100644 index 000000000..969e60a49 Binary files /dev/null and b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smith-chart.jpg differ diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartAdmittance.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartAdmittance.ts new file mode 100644 index 000000000..bc9b738c8 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartAdmittance.ts @@ -0,0 +1,373 @@ +import { NumericAxis, TSciChart, Rect, DpiHelper } from "scichart"; +import { SciChartSurfaceBase } from "scichart/Charting/Visuals/SciChartSurfaceBase"; +import { WebGlRenderContext2D, ELineDrawMode } from "scichart/Charting/Drawing/WebGlRenderContext2D"; +import { SCRTPen } from "scichart/types/TSciChart"; +import { + getArcParams, + getVectorArcVertex, + getArcVertex, + getVectorColorVertex, + getVertex, +} from "scichart/Charting/Visuals/Helpers/NativeObject"; +import { TickProvider } from "scichart/Charting/Numerics/TickProviders/TickProvider"; +import { NumberRange } from "scichart/Core/NumberRange"; +import { SmithGridCalculator } from "./smithChartGridCalculator"; +import { SmithChartAxisRenderer } from "./smithChartAxes"; + +const FALLBACK_ADMITTANCE_MAJOR = [0.2, 0.5, 1, 2, 5, 10, 20, 50]; + +export class SmithAdmittanceGTickProvider extends TickProvider { + constructor(_wasmContext: TSciChart) { + super(); + } + + getMajorTicks(_minorDelta: number, _majorDelta: number, _visibleRange: NumberRange): number[] { + return this._runCompute()?.rMajor ?? FALLBACK_ADMITTANCE_MAJOR; + } + + getMinorTicks(_minorDelta: number, _majorDelta: number, _visibleRange: NumberRange): number[] { + return this._runCompute()?.rMinor ?? []; + } + + private _runCompute(): SmithGridCalculator | null { + const gAxis = this.parentAxis as SmithChartAdmittanceResistanceAxis | undefined; + if (!gAxis?.gridCalculator) return null; + const surface = (gAxis as any).parentSurface; + if (!surface) return null; + const xRange: NumberRange = gAxis.visibleRange; + const bAxis = surface.yAxes?.get(1); + const yRange: NumberRange = bAxis?.visibleRange ?? xRange; + const xCalc = gAxis.getCurrentCoordinateCalculator(); + const yCalc = bAxis?.getCurrentCoordinateCalculator() ?? xCalc; + gAxis.gridCalculator.compute(xRange, yRange, xCalc, yCalc); + return gAxis.gridCalculator; + } +} + +export class SmithAdmittanceBTickProvider extends TickProvider { + constructor(_wasmContext: TSciChart) { + super(); + } + + getMajorTicks(_minorDelta: number, _majorDelta: number, _visibleRange: NumberRange): number[] { + return this._runCompute()?.xMajor ?? FALLBACK_ADMITTANCE_MAJOR; + } + + getMinorTicks(_minorDelta: number, _majorDelta: number, _visibleRange: NumberRange): number[] { + return this._runCompute()?.xMinor ?? []; + } + + private _runCompute(): SmithGridCalculator | null { + const bAxis = this.parentAxis; + if (!bAxis) return null; + // parentSurface is on AxisBase2D, not AxisCore (TickProvider's parentAxis type) — cast required + const surface = (bAxis as any).parentSurface; + if (!surface) return null; + // Admittance G-axis is at xAxes[1] — Z axes register first in drawExample.ts + const gAxis = surface.xAxes?.get(1) as SmithChartAdmittanceResistanceAxis | undefined; + if (!gAxis?.gridCalculator) return null; + const xRange: NumberRange = gAxis.visibleRange; + const yRange: NumberRange = bAxis.visibleRange; + const xCalc = gAxis.getCurrentCoordinateCalculator(); + const yCalc = bAxis.getCurrentCoordinateCalculator(); + gAxis.gridCalculator.compute(xRange, yRange, xCalc, yCalc); + return gAxis.gridCalculator; + } +} + +// ── Admittance resistance axis (constant-G circles) ────────────────────────── +// Mirror of constant-R: centre = (-g/(1+g), 0), radius = 1/(1+g) + +export class SmithChartAdmittanceResistanceAxis extends NumericAxis { + private sibling: SmithChartAdmittanceReactanceAxis | null = null; + public readonly gridCalculator = new SmithGridCalculator("admittance"); + + constructor(wasmContext: TSciChart, options?: object) { + super(wasmContext, { + drawLabels: false, + drawMajorTickLines: false, + drawMinorTickLines: false, + drawMajorBands: false, + zoomExtentsToInitialRange: true, + ...options, + }); + this.tickProvider = new SmithAdmittanceGTickProvider(wasmContext); + this.axisRenderer = new SmithChartAxisRenderer(wasmContext, true, true); + } + + // SciChart only draws grid lines for isPrimaryAxis axes. Admittance axes are + // always secondary (Z axes attach first). Override getter so this axis + // participates in grid drawing whenever it is visible, without disturbing + // the Z axis isPrimaryAxis state. + override get isPrimaryAxis(): boolean { + return this.isVisible; + } + override set isPrimaryAxis(_: boolean) { + // Suppress: visibility-based participation is managed by the getter above. + } + + override measure(): void { + super.measure(); + this.sibling = (this.parentSurface?.yAxes?.get(1) as SmithChartAdmittanceReactanceAxis) ?? null; + } + + protected override drawGridLines( + renderContext: WebGlRenderContext2D, + _tickCoords: number[], + linesPen: SCRTPen, + isMajor: boolean + ): void { + if (!this.sibling) return; + const wasmContext = this.webAssemblyContext2D; + const xCalc = this.getCurrentCoordinateCalculator(); + const yCalc = this.sibling.getCurrentCoordinateCalculator(); + const vpHeight = + SciChartSurfaceBase.domMasterCanvas?.height ?? this.parentSurface.renderSurface.viewportSize.height; + const clipRect = Rect.intersect(this.parentSurface.clipRect, this.parentSurface.seriesViewRect); + const leftPad = (this.parentSurface.padding?.left ?? 0) * DpiHelper.PIXEL_RATIO; + const topPad = (this.parentSurface.padding?.top ?? 0) * DpiHelper.PIXEL_RATIO; + const aspectRatio = Math.abs(xCalc.getCoordWidth(1)) / Math.abs(yCalc.getCoordWidth(1)); + const tp = this.tickProvider as any; + const vecArcs = getVectorArcVertex(wasmContext); + const arc = getArcVertex(wasmContext); + + const drawGCircle = (g: number, gapDistance: number) => { + const cx = -(g / (1 + g)); + const rad = 1 / (1 + g); + const sinHalfGap = gapDistance / (2 * rad); + if (sinHalfGap >= 1) return; + const arcGap = sinHalfGap > 0 ? 2 * Math.asin(sinHalfGap) : 0; + const cx_px = xCalc.getCoordinate(cx); + const cy_native = vpHeight - yCalc.getCoordinate(0); + const radius_px = Math.abs(xCalc.getCoordWidth(rad)); + // Gap centred at angle π — the tangent point (-1,0) where all G-circles converge. + arc.MakeCircularArc( + getArcParams( + wasmContext, + cx_px, + cy_native, + 0, + Math.PI - arcGap, + radius_px, + 0, + 1, + aspectRatio, + linesPen.m_fThickness + ) + ); + vecArcs.push_back(arc); + if (arcGap < Math.PI) { + arc.MakeCircularArc( + getArcParams( + wasmContext, + cx_px, + cy_native, + Math.PI + arcGap, + 2 * Math.PI, + radius_px, + 0, + 1, + aspectRatio, + linesPen.m_fThickness + ) + ); + vecArcs.push_back(arc); + } + }; + + if (isMajor) { + for (const g of tp.getMajorTicks(0, 0, null) as number[]) { + drawGCircle(g, 0); // full circles + } + // Flush G-circle arcs BEFORE calling getVectorArcVertex/getArcVertex again — + // those helpers return WASM singletons and a second call would reset vecArcs/arc. + if (vecArcs.size() > 0) { + renderContext.drawArcs(vecArcs, 0, 0, 0, clipRect, linesPen, undefined, leftPad, topPad); + } + + // Unit circle (radius=1, centre=(0,0)) — drawn here so it appears in Y-only mode + const ucVec = getVectorArcVertex(wasmContext); + const ucArc = getArcVertex(wasmContext); + ucArc.MakeCircularArc( + getArcParams( + wasmContext, + xCalc.getCoordinate(0), + vpHeight - yCalc.getCoordinate(0), + 0, + 2 * Math.PI, + Math.abs(xCalc.getCoordWidth(1)), + 0, + 1, + aspectRatio, + linesPen.m_fThickness * 2 + ) + ); + ucVec.push_back(ucArc); + renderContext.drawArcs(ucVec, 0, 0, 0, clipRect, linesPen, undefined, leftPad, topPad); + + // Real axis line from (-1,0) to (1,0) + const vertices = getVectorColorVertex(wasmContext); + const vertex = getVertex(wasmContext, 0, 0); + vertex.SetPosition(xCalc.getCoordinate(-1), yCalc.getCoordinate(0)); + vertices.push_back(vertex); + vertex.SetPosition(xCalc.getCoordinate(1), yCalc.getCoordinate(0)); + vertices.push_back(vertex); + renderContext.drawLinesNative( + vertices, + linesPen, + ELineDrawMode.DiscontinuousLine, + clipRect, + leftPad, + topPad + ); + } else { + // Minor ticks are encoded as flat pairs [g, bClip, g, bClip, ...] + const minor = tp.getMinorTicks(0, 0, null) as number[]; + for (let i = 0; i + 1 < minor.length; i += 2) { + const g = minor[i]; + const bClip = minor[i + 1]; + drawGCircle(g, 2 / Math.sqrt((g + 1) * (g + 1) + bClip * bClip)); + } + if (vecArcs.size() > 0) { + renderContext.drawArcs(vecArcs, 0, 0, 0, clipRect, linesPen, undefined, leftPad, topPad); + } + } + } +} + +// ── Admittance reactance axis (constant-B arcs) ─────────────────────────────── +// Mirror of constant-X arcs: centre = (-1, ±1/b), radius = 1/b +// Arc runs from (-1, 0) to unit circle intersection: xInt=(1-b²)/(1+b²), yInt=±2b/(1+b²) + +export class SmithChartAdmittanceReactanceAxis extends NumericAxis { + private sibling: SmithChartAdmittanceResistanceAxis | null = null; + + constructor(wasmContext: TSciChart, options?: object) { + super(wasmContext, { + drawLabels: false, + drawMajorTickLines: false, + drawMinorTickLines: false, + drawMajorBands: false, + zoomExtentsToInitialRange: true, + ...options, + }); + this.tickProvider = new SmithAdmittanceBTickProvider(wasmContext); + this.axisRenderer = new SmithChartAxisRenderer(wasmContext, false, true); + } + + override get isPrimaryAxis(): boolean { + return this.isVisible; + } + override set isPrimaryAxis(_: boolean) { + // Suppress: see SmithChartAdmittanceResistanceAxis for rationale. + } + + override measure(): void { + super.measure(); + this.sibling = (this.parentSurface?.xAxes?.get(1) as SmithChartAdmittanceResistanceAxis) ?? null; + } + + protected override drawGridLines( + renderContext: WebGlRenderContext2D, + _tickCoords: number[], + linesPen: SCRTPen, + isMajor: boolean + ): void { + if (!this.sibling) return; + const wasmContext = this.webAssemblyContext2D; + const xCalc = this.sibling.getCurrentCoordinateCalculator(); + const yCalc = this.getCurrentCoordinateCalculator(); + const vpHeight = + SciChartSurfaceBase.domMasterCanvas?.height ?? this.parentSurface.renderSurface.viewportSize.height; + const clipRect = Rect.intersect(this.parentSurface.clipRect, this.parentSurface.seriesViewRect); + const leftPad = (this.parentSurface.padding?.left ?? 0) * DpiHelper.PIXEL_RATIO; + const topPad = (this.parentSurface.padding?.top ?? 0) * DpiHelper.PIXEL_RATIO; + const aspectRatio = Math.abs(xCalc.getCoordWidth(1)) / Math.abs(yCalc.getCoordWidth(1)); + const tp = this.tickProvider as any; + const vecArcs = getVectorArcVertex(wasmContext); + const arc = getArcVertex(wasmContext); + + const drawBArc = (absB: number, gapDistance: number) => { + const rad = 1 / absB; + const cx_data = -1; + + const cy_pos = rad; + const cy_neg = -rad; + + const bv2 = absB * absB; + const xInt = (1 - bv2) / (1 + bv2); + const yInt_pos = (2 * absB) / (1 + bv2); + const yInt_neg = -yInt_pos; + + let posStart = Math.atan2(-cy_pos, 0); + let posEnd = Math.atan2(yInt_pos - cy_pos, xInt - cx_data); + while (posEnd <= posStart) posEnd += 2 * Math.PI; + + const negStart = Math.atan2(yInt_neg - cy_neg, xInt - cx_data); + let negEnd = Math.atan2(-cy_neg, 0); + while (negEnd <= negStart) negEnd += 2 * Math.PI; + + if (gapDistance > 0) { + const sinHalfGap = gapDistance / (2 * rad); + if (sinHalfGap >= 1) return; + const arcGap = 2 * Math.asin(sinHalfGap); + posStart += arcGap; + negEnd -= arcGap; + if (posEnd <= posStart || negEnd <= negStart) return; + } + + const cx_px = xCalc.getCoordinate(cx_data); + const rad_px = Math.abs(xCalc.getCoordWidth(rad)); + + arc.MakeCircularArc( + getArcParams( + wasmContext, + cx_px, + vpHeight - yCalc.getCoordinate(cy_pos), + posStart, + posEnd, + rad_px, + 0, + 1, + aspectRatio, + linesPen.m_fThickness + ) + ); + vecArcs.push_back(arc); + + arc.MakeCircularArc( + getArcParams( + wasmContext, + cx_px, + vpHeight - yCalc.getCoordinate(cy_neg), + negStart, + negEnd, + rad_px, + 0, + 1, + aspectRatio, + linesPen.m_fThickness + ) + ); + vecArcs.push_back(arc); + }; + + if (isMajor) { + for (const b of tp.getMajorTicks(0, 0, null) as number[]) { + drawBArc(b, 0); // full arcs + } + } else { + // Minor ticks are encoded as flat pairs [b, gClip, b, gClip, ...] + const minor = tp.getMinorTicks(0, 0, null) as number[]; + for (let i = 0; i + 1 < minor.length; i += 2) { + const b = minor[i]; + const gClip = minor[i + 1]; + drawBArc(b, 2 / Math.sqrt((gClip + 1) * (gClip + 1) + b * b)); + } + } + + if (vecArcs.size() > 0) { + renderContext.drawArcs(vecArcs, 0, 0, 0, clipRect, linesPen, undefined, leftPad, topPad); + } + } +} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartAxes.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartAxes.ts new file mode 100644 index 000000000..50e6a4739 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartAxes.ts @@ -0,0 +1,799 @@ +// ── Pure math helpers (exported for testing) ────────────────────────────────── + +/** Returns the data-space centre and radius of the constant-R circle. */ +export function rCircleParams(r: number): { cx: number; cy: number; rad: number } { + return { cx: r / (1 + r), cy: 0, rad: 1 / (1 + r) }; +} + +/** Returns the data-space centre and radius of the constant-X arc circle. */ +export function xArcCircleCenter(absX: number, isPositive: boolean): { cx: number; cy: number; rad: number } { + const rad = 1 / absX; + return { cx: 1, cy: isPositive ? rad : -rad, rad }; +} + +/** + * Returns start/end angles (radians, CCW in Y-up space) for the + * constant-X arc clipped to the unit circle. + * The arc runs from the unit-circle intersection to the point (1, 0). + */ +export function xArcAngles(absX: number, isPositive: boolean): { startAngle: number; endAngle: number } { + const xv2 = absX * absX; + const xInt = (xv2 - 1) / (1 + xv2); + const yInt = isPositive ? (2 * absX) / (1 + xv2) : -(2 * absX) / (1 + xv2); + const cy = isPositive ? 1 / absX : -1 / absX; + + const thetaToIntersection = Math.atan2(yInt - cy, xInt - 1); + const thetaToOrigin = isPositive ? -Math.PI / 2 : Math.PI / 2; + + let startAngle: number; + let endAngle: number; + + if (isPositive) { + startAngle = thetaToIntersection; + endAngle = thetaToOrigin; + while (endAngle <= startAngle) endAngle += 2 * Math.PI; + } else { + startAngle = thetaToOrigin; + endAngle = thetaToIntersection; + while (endAngle <= startAngle) endAngle += 2 * Math.PI; + } + + return { startAngle, endAngle }; +} + +import { + NumericAxis, + TSciChart, + Rect, + DpiHelper, + TGridLineStyle, + TTextStyle, + TTickLineStyle, + deleteSafe, + EAxisAlignment, + LabelProviderBase2D, + LabelInfo, + AxisBase2D, +} from "scichart"; +import { Pen2DCache } from "scichart/Charting/Drawing/Pen2DCache"; +import { INumericAxisOptions } from "scichart/Charting/Visuals/Axis/NumericAxis"; +import { SciChartSurfaceBase } from "scichart/Charting/Visuals/SciChartSurfaceBase"; +import { drawRimTicks, RimConfig, DEFAULT_RIM_TICK_SIZE } from "./smithChartRim"; +import { TickProvider } from "scichart/Charting/Numerics/TickProviders/TickProvider"; +import { NumberRange } from "scichart/Core/NumberRange"; +import { SmithGridCalculator } from "./smithChartGridCalculator"; +import { WebGlRenderContext2D, ELineDrawMode } from "scichart/Charting/Drawing/WebGlRenderContext2D"; +import { SCRTPen } from "scichart/types/TSciChart"; +import { + getArcParams, + getVectorArcVertex, + getArcVertex, + getVectorColorVertex, + getVertex, + getTextBounds, + getVector4, +} from "scichart/Charting/Visuals/Helpers/NativeObject"; +import { AxisRenderer } from "scichart/Charting/Visuals/Axis/AxisRenderer"; +import { parseColorToUIntArgb } from "scichart/utils/parseColor"; +import { convertMultiLineAlignment } from "scichart/types/TextPosition"; + +// ── Tick Providers ──────────────────────────────────────────────────────────── + +/** + * Minor ticks are encoded as flat pairs: [value, clipOtherValue, value, clipOtherValue, ...]. + * + * The arc for a given `value` stops at the point where it intersects the OTHER family's arc + * at `clipOtherValue`. The gap distance is: + * + * D = 2 / sqrt((R+1)² + X²) + * + * where (R, X) is the intersection of the constant-R circle and constant-X arc. + * For a resistance-axis minor arc at R=r clipped to X=xClip: D = 2/sqrt((r+1)²+xClip²) + * For a reactance-axis minor arc at X=x clipped to R=rClip: D = 2/sqrt((rClip+1)²+x²) + * + * Multiple tiers are interleaved in the same array using different clipOtherValue settings. + * A smaller clipOtherValue → larger D → shorter arc → only visible for the bigger circles + * (left side of chart), naturally creating denser coverage there. + */ +const FALLBACK_MAJOR = [0.2, 0.5, 1, 2, 5, 10, 20, 50]; + +export class SmithResistanceTickProvider extends TickProvider { + constructor(_wasmContext: TSciChart) { + super(); + } + + getMajorTicks(_minorDelta: number, _majorDelta: number, _visibleRange: NumberRange): number[] { + return this._runCompute()?.rMajor ?? FALLBACK_MAJOR; + } + + getMinorTicks(_minorDelta: number, _majorDelta: number, _visibleRange: NumberRange): number[] { + return this._runCompute()?.rMinor ?? []; + } + + private _runCompute(): SmithGridCalculator | null { + const rAxis = this.parentAxis as SmithChartResistanceAxis | undefined; + if (!rAxis || !rAxis.gridCalculator) return null; + const surface = rAxis.parentSurface; + if (!surface) return null; + const xRange: NumberRange = rAxis.visibleRange; + const yAxis = surface.yAxes?.get(0); + const yRange: NumberRange = yAxis?.visibleRange ?? xRange; + const xCalc = rAxis.getCurrentCoordinateCalculator(); + const yCalc = yAxis?.getCurrentCoordinateCalculator() ?? xCalc; + rAxis.gridCalculator.compute(xRange, yRange, xCalc, yCalc); + return rAxis.gridCalculator; + } +} + +export class SmithReactanceTickProvider extends TickProvider { + constructor(_wasmContext: TSciChart) { + super(); + } + + getMajorTicks(_minorDelta: number, _majorDelta: number, _visibleRange: NumberRange): number[] { + return this._runCompute()?.xMajor ?? FALLBACK_MAJOR; + } + + getMinorTicks(_minorDelta: number, _majorDelta: number, _visibleRange: NumberRange): number[] { + return this._runCompute()?.xMinor ?? []; + } + + private _runCompute(): SmithGridCalculator | null { + const yAxis = this.parentAxis as AxisBase2D | undefined; + if (!yAxis) return null; + const surface = yAxis.parentSurface; + if (!surface) return null; + const rAxis = surface.xAxes?.get(0) as SmithChartResistanceAxis | undefined; + if (!rAxis?.gridCalculator) return null; + const xRange: NumberRange = rAxis.visibleRange; + const yRange: NumberRange = yAxis.visibleRange; + const xCalc = rAxis.getCurrentCoordinateCalculator(); + const yCalc = yAxis.getCurrentCoordinateCalculator(); + rAxis.gridCalculator.compute(xRange, yRange, xCalc, yCalc); + return rAxis.gridCalculator; + } +} + +// ── SmithChartAxisRenderer ──────────────────────────────────────────────────── + +/** + * Draws Smith chart axis labels directly onto the series view rect. + * Supports both the native-text (DrawStringAdvanced) and texture (drawTexture) + * rendering paths, matching the pattern used by PolarAxisRenderer. + */ +export class SmithChartAxisRenderer extends AxisRenderer { + private readonly _isResistanceAxis: boolean; + private readonly _isAdmittance: boolean; + + constructor(wasmContext: TSciChart, isResistanceAxis: boolean, isAdmittance = false) { + super(wasmContext); + this._isResistanceAxis = isResistanceAxis; + this._isAdmittance = isAdmittance; + } + + /** No axis margin needed — labels are drawn inside the series view rect. */ + public override measure( + _isHorizontalAxis: boolean, + _labelStyle: TTextStyle, + _majorTickLabels: string[], + _ticksSize: number, + _labelProvider: LabelProviderBase2D, + _drawLabels: boolean, + _drawTicks: boolean, + _labelInfos?: LabelInfo[] + ): void { + this.desiredHeight = 0; + this.desiredWidth = 0; + this.desiredTicksSize = 0; + } + + public override drawLabels( + renderContext: WebGlRenderContext2D, + _axisAlignment: EAxisAlignment, + _isInnerAxis: boolean, + _tickLabels: string[], + _tickCoords: number[], + _axisOffset: number, + labelStyle: TTextStyle, + _isVerticalChart: boolean, + _isFlippedCoordinates: boolean, + labelProvider: LabelProviderBase2D, + _labelInfos?: LabelInfo[] + ): void { + const axis = this.parentAxis; + const surface = axis?.parentSurface; + if (!surface) return; + + const svr: Rect = surface.seriesViewRect; + renderContext.resetAndClip(svr); + + const xCalc = surface.xAxes.get(0).getCurrentCoordinateCalculator(); + const yCalc = surface.yAxes.get(0).getCurrentCoordinateCalculator(); + + // ── Set up rendering paths (mirrors PolarAxisRenderer pattern) ───────── + const { multilineAlignment } = labelStyle; + const textColor = parseColorToUIntArgb(labelStyle.color); + const wasmCtx = this.webAssemblyContext; + + const nativeFont = labelProvider.useNativeText ? renderContext.getFont(labelStyle, true) : null; + const textBounds = nativeFont ? getTextBounds(wasmCtx) : null; + // convertMultiLineAlignment called unconditionally, as in PolarAxisRenderer + const mlaNative = convertMultiLineAlignment(multilineAlignment, wasmCtx); + const nativeLineSpacing = labelProvider.lineSpacing; + const lineHeight = this.calculateLineHightForNativeFont(nativeFont, textBounds); + + const drawLabel = (text: string, px: number, py: number, outwardAngle: number) => { + try { + if (nativeFont) { + nativeFont.CalculateStringBounds(text, textBounds, nativeLineSpacing); + const w = textBounds.m_fWidth; + const h = textBounds.m_fHeight; + const { dx, dy } = SmithChartAxisRenderer._positionLabel(w, h, outwardAngle); + const tx = Math.round(px + svr.left + dx); + const ty = Math.round(py + svr.top + dy); + nativeFont.DrawStringAdvanced( + text, + textColor, + tx, + ty + lineHeight, + getVector4(wasmCtx, tx, ty + lineHeight, 0, 0), + mlaNative, + nativeLineSpacing + ); + } else { + const { + bitmapTexture, + textureWidth: w, + textureHeight: h, + } = labelProvider.getCachedLabelTexture(text, this.textureManager, labelStyle); + if (!bitmapTexture) return; + const { dx, dy } = SmithChartAxisRenderer._positionLabel(w, h, outwardAngle); + const tx = Math.round(px + svr.left + dx); + const ty = Math.round(py + svr.top + dy); + renderContext.drawTexture(bitmapTexture, tx, ty, w, h); + if (!labelProvider.useCache) bitmapTexture.delete(); + } + } catch (_err) { + // WebGL context lost — clear label cache so it rebuilds next frame + labelProvider.delete?.(); + } + }; + + // Call getMajorTicks directly rather than using the ticks already computed + // by AxisBase2D.getTicks(). AxisBase2D filters ticks to visibleRange (e.g. [-1.15, 1.15]), + // which discards most Smith chart R/X values (e.g. 2, 5, 10, 20, 50). The renderer + // fetches them unfiltered here so all circle/arc labels are placed correctly. + const tp = axis?.tickProvider; + const majorTicks: number[] = tp?.getMajorTicks(0, 0, axis.visibleRange) ?? []; + + if (this._isResistanceAxis) { + if (this._isAdmittance) { + // Admittance G-labels: G=0 at (1,0), G=g at ((1-g)/(1+g), 0) + drawLabel("0", xCalc.getCoordinate(1), yCalc.getCoordinate(0), Math.PI); + for (const g of majorTicks) { + drawLabel( + labelProvider.formatLabel(g), + xCalc.getCoordinate((1 - g) / (1 + g)), + yCalc.getCoordinate(0), + Math.PI / 2 + ); + } + if (nativeFont) nativeFont.End(); + return; + } + + const rAxis = axis as SmithChartResistanceAxis; + if (rAxis.drawGridLabels !== false) { + drawLabel("0", xCalc.getCoordinate(-1), yCalc.getCoordinate(0), Math.PI); + for (const r of majorTicks) { + const { cx, rad } = rCircleParams(r); + drawLabel( + labelProvider.formatLabel(r), + xCalc.getCoordinate(cx - rad), + yCalc.getCoordinate(0), + Math.PI / 2 + ); + } + } + + const sel = rAxis.selectedIntersection; + if (sel !== null) { + const x0 = sel.x; + const xArcCy = 1 / x0; + for (const rj of majorTicks) { + const denom = (rj + 1) ** 2 + x0 * x0; + const gr = (rj * rj + x0 * x0 - 1) / denom; + const gi = (2 * x0) / denom; + const angleRad = Math.atan2(gi - xArcCy, gr - 1); + drawLabel( + labelProvider.formatLabel(rj), + xCalc.getCoordinate(gr), + yCalc.getCoordinate(gi), + angleRad + ); + } + } + + // End main font batch before rim labels, which may use a different style. + if (nativeFont) nativeFont.End(); + + // Rim labels (angle-of-Γ ring) — use rimLabelStyle if set, else reuse labelStyle. + const rimLabels = rAxis._pendingRimLabels ?? []; + if (rimLabels.length > 0) { + const rimStyle = rAxis.rimLabelStyle ? { ...labelStyle, ...rAxis.rimLabelStyle } : labelStyle; + const rimTextColor = parseColorToUIntArgb(rimStyle.color); + const rimFont = labelProvider.useNativeText ? renderContext.getFont(rimStyle, true) : null; + const rimLineH = this.calculateLineHightForNativeFont(rimFont, textBounds); + for (const lbl of rimLabels) { + const angleRad = (lbl.angleDeg * Math.PI) / 180; + try { + if (rimFont) { + rimFont.CalculateStringBounds(lbl.text, textBounds, nativeLineSpacing); + const w = textBounds.m_fWidth; + const h = textBounds.m_fHeight; + const { dx, dy } = SmithChartAxisRenderer._positionLabel(w, h, angleRad, 0); + const tx = Math.round(lbl.px + svr.left + dx); + const ty = Math.round(lbl.py + svr.top + dy); + rimFont.DrawStringAdvanced( + lbl.text, + rimTextColor, + tx, + ty + rimLineH, + getVector4(wasmCtx, tx, ty + rimLineH, 0, 0), + mlaNative, + nativeLineSpacing + ); + } else { + const { + bitmapTexture, + textureWidth: w, + textureHeight: h, + } = labelProvider.getCachedLabelTexture(lbl.text, this.textureManager, rimStyle); + if (!bitmapTexture) continue; + const { dx, dy } = SmithChartAxisRenderer._positionLabel(w, h, angleRad, 0); + const tx = Math.round(lbl.px + svr.left + dx); + const ty = Math.round(lbl.py + svr.top + dy); + renderContext.drawTexture(bitmapTexture, tx, ty, w, h); + if (!labelProvider.useCache) bitmapTexture.delete(); + } + } catch (_err) { + labelProvider.delete?.(); + } + } + if (rimFont) rimFont.End(); + } + + return; // nativeFont already ended above + } else { + if (this._isAdmittance) { + // B-arc labels at unit-circle intersection: ((1-b²)/(1+b²), ±2b/(1+b²)) + for (const b of majorTicks) { + const bv2 = b * b; + const lx = (1 - bv2) / (1 + bv2); + const ly = (2 * b) / (1 + bv2); + const label = labelProvider.formatLabel(b); + drawLabel(`+${label}`, xCalc.getCoordinate(lx), yCalc.getCoordinate(ly), Math.atan2(ly, lx)); + drawLabel(`-${label}`, xCalc.getCoordinate(lx), yCalc.getCoordinate(-ly), Math.atan2(-ly, lx)); + } + if (nativeFont) nativeFont.End(); + return; + } + + for (const x of majorTicks) { + const xv2 = x * x; + const lx = (xv2 - 1) / (1 + xv2); + const ly = (2 * x) / (1 + xv2); + const label = labelProvider.formatLabel(x); + drawLabel(`+${label}`, xCalc.getCoordinate(lx), yCalc.getCoordinate(ly), Math.atan2(ly, lx)); + drawLabel(`-${label}`, xCalc.getCoordinate(lx), yCalc.getCoordinate(-ly), Math.atan2(-ly, lx)); + } + + const rAxisForSel = surface?.xAxes?.get(0) as SmithChartResistanceAxis | undefined; + const sel = rAxisForSel?.selectedIntersection ?? null; + if (sel !== null) { + const r0 = sel.r; + const rCircleCx = r0 / (1 + r0); + for (const xj of majorTicks) { + const denom = (r0 + 1) ** 2 + xj * xj; + const gr = (r0 * r0 + xj * xj - 1) / denom; + const gi_pos = (2 * xj) / denom; + const gi_neg = -gi_pos; + const text = labelProvider.formatLabel(xj); + drawLabel( + `+${text}`, + xCalc.getCoordinate(gr), + yCalc.getCoordinate(gi_pos), + Math.atan2(gi_pos, gr - rCircleCx) + ); + drawLabel( + `-${text}`, + xCalc.getCoordinate(gr), + yCalc.getCoordinate(gi_neg), + Math.atan2(gi_neg, gr - rCircleCx) + ); + } + } + } + + if (nativeFont) nativeFont.End(); + } + + private static _positionLabel(w: number, h: number, angleRad: number, gap = 4): { dx: number; dy: number } { + const GAP = gap; + const CARDINAL_DEG = 10; + + const aDeg = Math.round((angleRad * 180) / Math.PI); + const absModDeg = Math.abs(aDeg) % 90; + const isCardinal = absModDeg < CARDINAL_DEG || absModDeg > 90 - CARDINAL_DEG; + const quadrant = ((((aDeg + CARDINAL_DEG) / 90) % 4) + 4) % 4; + + let dx = 0; + let dy = 0; + + if (isCardinal) { + if (quadrant < 1) { + dy = -h / 2; + } else if (quadrant < 2) { + dx = -w / 2; + dy = -h; + } else if (quadrant < 3) { + dx = -w; + dy = -h / 2; + } else { + dx = -w / 2; + } + } else { + if (Math.cos(angleRad) < 0) dx = -w; + if (Math.sin(angleRad) > 0) dy = -h; + } + + dx += GAP * Math.cos(angleRad); + dy -= GAP * Math.sin(angleRad); + + return { dx, dy }; + } +} + +// ── SmithChartResistanceAxis (X axis) — draws constant-R circles ────────────── + +export interface ISmithChartResistanceAxisOptions extends INumericAxisOptions { + rimMajorTickStep?: number; + rimMinorTickStep?: number; + rimLabelOffset?: number; + rimLabelStyle?: TTextStyle; + rimTickLineStyle?: TTickLineStyle; + rimLineStyle?: TGridLineStyle; +} + +export class SmithChartResistanceAxis extends NumericAxis { + private sibling: SmithChartReactanceAxis | null = null; + public _pendingRimLabels: { text: string; px: number; py: number; angleDeg: number }[] = []; + /** Set by SmithMarkersAdapter to drive intersection labels. null = no active marker. */ + public selectedIntersection: { r: number; x: number } | null = null; + /** Controls R/X grid labels independently of rim labels (rim always draws). */ + public drawGridLabels = true; + public readonly gridCalculator = new SmithGridCalculator("impedance"); + public rimConfig: RimConfig; + public rimLabelStyle?: TTextStyle; + public rimTickLineStyle?: TTickLineStyle; + public rimLineStyle?: TGridLineStyle; + private _rimLinePenCache: Pen2DCache; + private _rimTickLinePenCache: Pen2DCache; + + constructor(wasmContext: TSciChart, options?: ISmithChartResistanceAxisOptions) { + super(wasmContext, { + drawLabels: true, + drawMajorTickLines: false, + drawMinorTickLines: false, + drawMajorBands: false, + zoomExtentsToInitialRange: true, + ...options, + }); + this.tickProvider = new SmithResistanceTickProvider(wasmContext); + this.axisRenderer = new SmithChartAxisRenderer(wasmContext, true); + this.rimConfig = { + majorTickStep: options?.rimMajorTickStep, + minorTickStep: options?.rimMinorTickStep, + labelOffset: options?.rimLabelOffset, + }; + this.rimLabelStyle = options?.rimLabelStyle; + this.rimTickLineStyle = options?.rimTickLineStyle; + this.rimLineStyle = options?.rimLineStyle; + this._rimLinePenCache = new Pen2DCache(wasmContext); + this._rimTickLinePenCache = new Pen2DCache(wasmContext); + } + + override measure(): void { + super.measure(); + this.sibling = (this.parentSurface?.yAxes?.get(0) as SmithChartReactanceAxis) ?? null; + } + + public override delete(): void { + this._rimLinePenCache = deleteSafe(this._rimLinePenCache); + this._rimTickLinePenCache = deleteSafe(this._rimTickLinePenCache); + super.delete(); + } + + protected override drawGridLines( + renderContext: WebGlRenderContext2D, + _tickCoords: number[], + linesPen: SCRTPen, + isMajor: boolean + ): void { + if (!this.sibling) return; + + const wasmContext = this.webAssemblyContext2D; + const xCalc = this.getCurrentCoordinateCalculator(); + const yCalc = this.sibling.getCurrentCoordinateCalculator(); + // Use master canvas height so the y-flip matches the C++ matTransform. + // viewportSize tracks the visible (target) canvas which shrinks on resize; + // the master WebGL canvas only grows, and SCRTSetMainWindowSize uses its height. + const vpHeight = + SciChartSurfaceBase.domMasterCanvas?.height ?? this.parentSurface.renderSurface.viewportSize.height; + const clipRect = Rect.intersect(this.parentSurface.clipRect, this.parentSurface.seriesViewRect); + const leftPad = (this.parentSurface.padding?.left ?? 0) * DpiHelper.PIXEL_RATIO; + const topPad = (this.parentSurface.padding?.top ?? 0) * DpiHelper.PIXEL_RATIO; + + const aspectRatio = Math.abs(xCalc.getCoordWidth(1)) / Math.abs(yCalc.getCoordWidth(1)); + const tp = this.tickProvider; + + const vecArcs = getVectorArcVertex(wasmContext); + const arc = getArcVertex(wasmContext); + + const drawArc = (r: number, gapDistance: number) => { + const { cx, cy, rad } = rCircleParams(r); + const sinHalfGap = gapDistance / (2 * rad); + if (sinHalfGap >= 1) return; + const arcGap = sinHalfGap > 0 ? 2 * Math.asin(sinHalfGap) : 0; + const cx_px = xCalc.getCoordinate(cx); + const cy_native = vpHeight - yCalc.getCoordinate(cy); + const radius_px = Math.abs(xCalc.getCoordWidth(rad)); + if (gapDistance > 0 && (2 * Math.PI - 2 * arcGap) * radius_px < 10) return; + const arcParams = getArcParams( + wasmContext, + cx_px, + cy_native, + arcGap, + 2 * Math.PI - arcGap, + radius_px, + 0, + 1, + aspectRatio, + linesPen.m_fThickness + ); + arc.MakeCircularArc(arcParams); + vecArcs.push_back(arc); + }; + + if (isMajor) { + for (const r of tp.getMajorTicks(0, 0, this.visibleRange)) { + drawArc(r, 0); // full circles + } + } else { + // Minor ticks are encoded as flat pairs [r, xClip, r, xClip, ...] + const minor = tp.getMinorTicks(0, 0, this.visibleRange); + for (let i = 0; i + 1 < minor.length; i += 2) { + const r = minor[i]; + const xClip = minor[i + 1]; + drawArc(r, 2 / Math.sqrt((r + 1) * (r + 1) + xClip * xClip)); + } + } + + if (vecArcs.size() > 0) { + renderContext.drawArcs(vecArcs, 0, 0, 0, clipRect, linesPen, undefined, leftPad, topPad); + } + + // Draw the horizontal real axis line once, on the major pass + if (isMajor) { + const vertices = getVectorColorVertex(wasmContext); + const vertex = getVertex(wasmContext, 0, 0); + + vertex.SetPosition(xCalc.getCoordinate(-1), yCalc.getCoordinate(0)); + vertices.push_back(vertex); + vertex.SetPosition(xCalc.getCoordinate(1), yCalc.getCoordinate(0)); + vertices.push_back(vertex); + + renderContext.drawLinesNative( + vertices, + linesPen, + ELineDrawMode.DiscontinuousLine, + clipRect, + leftPad, + topPad + ); + + // Unit circle as hardware arc (radius=1, centre=(0,0)) + const ucVec = getVectorArcVertex(wasmContext); + const ucArc = getArcVertex(wasmContext); + const ucx_px = xCalc.getCoordinate(0); + const ucy_native = vpHeight - yCalc.getCoordinate(0); + const urad_px = Math.abs(xCalc.getCoordWidth(1)); + let ucPenPre = linesPen; + if (this.rimLineStyle) { + const rs = this.rimLineStyle; + ucPenPre = this.getPenForLines( + this._rimLinePenCache, + rs.color ?? this.majorGridLineStyle.color ?? "#aaaaaa", + rs.strokeThickness ?? this.majorGridLineStyle.strokeThickness ?? 2, + rs.strokeDashArray + ); + } + const ucThickness = this.rimLineStyle ? ucPenPre.m_fThickness : linesPen.m_fThickness * 2; + const ucParams = getArcParams( + wasmContext, + ucx_px, + ucy_native, + 0, + 2 * Math.PI, + urad_px, + 0, + 1, + aspectRatio, + ucThickness + ); + ucArc.MakeCircularArc(ucParams); + ucVec.push_back(ucArc); + renderContext.drawArcs(ucVec, 0, 0, 0, clipRect, ucPenPre, undefined, leftPad, topPad); + + // Angle-of-Γ rim ring ticks + labels + this._pendingRimLabels = []; + let rimPen = linesPen; + if (this.rimTickLineStyle) { + const rs = this.rimTickLineStyle; + rimPen = this.getPenForLines( + this._rimTickLinePenCache, + rs.color ?? this.majorGridLineStyle.color ?? "#aaaaaa", + rs.strokeThickness ?? this.majorGridLineStyle.strokeThickness ?? 1 + ); + } + const tickSizePx = this.rimTickLineStyle?.tickSize ?? DEFAULT_RIM_TICK_SIZE; + const rimLineHalfThicknessPx = ucPenPre.m_fThickness / 2; + drawRimTicks( + renderContext, + xCalc, + yCalc, + wasmContext, + rimPen, + clipRect, + leftPad, + topPad, + this.rimConfig, + tickSizePx, + rimLineHalfThicknessPx, + (text, px, py, angleDeg) => { + this._pendingRimLabels.push({ text, px, py, angleDeg }); + } + ); + } + } +} + +// ── SmithChartReactanceAxis (Y axis) — draws constant-X arcs ───────────────── + +export class SmithChartReactanceAxis extends NumericAxis { + private sibling: SmithChartResistanceAxis | null = null; + + constructor(wasmContext: TSciChart, options?: object) { + super(wasmContext, { + drawLabels: true, + drawMajorTickLines: false, + drawMinorTickLines: false, + drawMajorBands: false, + zoomExtentsToInitialRange: true, + ...options, + }); + this.tickProvider = new SmithReactanceTickProvider(wasmContext); + this.axisRenderer = new SmithChartAxisRenderer(wasmContext, false); + } + + override measure(): void { + super.measure(); + this.sibling = (this.parentSurface?.xAxes?.get(0) as SmithChartResistanceAxis) ?? null; + } + + protected override drawGridLines( + renderContext: WebGlRenderContext2D, + _tickCoords: number[], + linesPen: SCRTPen, + isMajor: boolean + ): void { + if (!this.sibling) return; + + const wasmContext = this.webAssemblyContext2D; + const xCalc = this.sibling.getCurrentCoordinateCalculator(); + const yCalc = this.getCurrentCoordinateCalculator(); + // Use master canvas height so the y-flip matches the C++ matTransform. + // viewportSize tracks the visible (target) canvas which shrinks on resize; + // the master WebGL canvas only grows, and SCRTSetMainWindowSize uses its height. + const vpHeight = + SciChartSurfaceBase.domMasterCanvas?.height ?? this.parentSurface.renderSurface.viewportSize.height; + const svr = this.parentSurface.seriesViewRect; + const clipRect = Rect.intersect(this.parentSurface.clipRect, svr); + const leftPad = (this.parentSurface.padding?.left ?? 0) * DpiHelper.PIXEL_RATIO; + const topPad = (this.parentSurface.padding?.top ?? 0) * DpiHelper.PIXEL_RATIO; + + const aspectRatio = Math.abs(xCalc.getCoordWidth(1)) / Math.abs(yCalc.getCoordWidth(1)); + const tp = this.tickProvider; + + const vecArcs = getVectorArcVertex(wasmContext); + const arc = getArcVertex(wasmContext); + + const drawArc = (absX: number, gapDistance: number) => { + const pos = xArcCircleCenter(absX, true); + const posAngles = xArcAngles(absX, true); + const posStart = posAngles.startAngle; + let posEnd = posAngles.endAngle; + + const neg = xArcCircleCenter(absX, false); + const negAngles = xArcAngles(absX, false); + let negStart = negAngles.startAngle; + const negEnd = negAngles.endAngle; + + if (gapDistance > 0) { + const sinHalfGap = gapDistance / (2 * pos.rad); + if (sinHalfGap >= 1) return; + const arcGap = 2 * Math.asin(sinHalfGap); + posEnd -= arcGap; + negStart += arcGap; + if (posEnd <= posStart || negEnd <= negStart) return; + } + + const pos_cx_px = xCalc.getCoordinate(pos.cx); + const pos_cy_native = vpHeight - yCalc.getCoordinate(pos.cy); + const pos_radius_px = Math.abs(xCalc.getCoordWidth(pos.rad)); + if (gapDistance > 0 && (posEnd - posStart) * pos_radius_px < 10) return; + arc.MakeCircularArc( + getArcParams( + wasmContext, + pos_cx_px, + pos_cy_native, + posStart, + posEnd, + pos_radius_px, + 0, + 1, + aspectRatio, + linesPen.m_fThickness + ) + ); + vecArcs.push_back(arc); + + const neg_cx_px = xCalc.getCoordinate(neg.cx); + const neg_cy_native = vpHeight - yCalc.getCoordinate(neg.cy); + const neg_radius_px = Math.abs(xCalc.getCoordWidth(neg.rad)); + arc.MakeCircularArc( + getArcParams( + wasmContext, + neg_cx_px, + neg_cy_native, + negStart, + negEnd, + neg_radius_px, + 0, + 1, + aspectRatio, + linesPen.m_fThickness + ) + ); + vecArcs.push_back(arc); + }; + + if (isMajor) { + for (const x of tp.getMajorTicks(0, 0, this.visibleRange)) { + drawArc(x, 0); // full arcs + } + } else { + // Minor ticks are encoded as flat pairs [x, rClip, x, rClip, ...] + const minor = tp.getMinorTicks(0, 0, this.visibleRange); + for (let i = 0; i + 1 < minor.length; i += 2) { + const x = minor[i]; + const rClip = minor[i + 1]; + drawArc(x, 2 / Math.sqrt((rClip + 1) * (rClip + 1) + x * x)); + } + } + + if (vecArcs.size() > 0) { + renderContext.drawArcs(vecArcs, 0, 0, 0, clipRect, linesPen, undefined, leftPad, topPad); + } + } +} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartChain.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartChain.ts new file mode 100644 index 000000000..ec278627e --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartChain.ts @@ -0,0 +1,152 @@ +import { SciChartSurface, TSciChart, XyDataSeries, FastLineRenderableSeries } from "scichart"; +import { GammaPoint, ChainStep, ComponentType, SmithState, SmithAction } from "./useSmithChart"; + +const ARC_STEPS = 100; + +export function computeChainStep( + fromGamma: GammaPoint, + type: ComponentType, + value: number, + frequency: number, + Z0: number +): { toGamma: GammaPoint; arcPoints: GammaPoint[] } { + const omega = 2 * Math.PI * frequency; + const { re: gr, im: gi } = fromGamma; + + // Γ → z = (1+Γ)/(1−Γ) + const dz = (1 - gr) ** 2 + gi ** 2; + const zr = dz > 1e-12 ? (1 - gr ** 2 - gi ** 2) / dz : 1e6; + const zx = dz > 1e-12 ? (2 * gi) / dz : 0; + + // z → y = 1/z + const zm2 = zr ** 2 + zx ** 2; + const gy = zm2 > 1e-12 ? zr / zm2 : 1e6; + const by = zm2 > 1e-12 ? -zx / zm2 : 0; + + if (type === "TL") { + const rotAngle = -4 * Math.PI * value; // clockwise = negative + const mag = Math.sqrt(gr ** 2 + gi ** 2); + const startAngle = Math.atan2(gi, gr); + const arcPoints: GammaPoint[] = []; + for (let i = 0; i <= ARC_STEPS; i++) { + const a = startAngle + (i / ARC_STEPS) * rotAngle; + arcPoints.push({ re: mag * Math.cos(a), im: mag * Math.sin(a) }); + } + const endAngle = startAngle + rotAngle; + return { toGamma: { re: mag * Math.cos(endAngle), im: mag * Math.sin(endAngle) }, arcPoints }; + } + + if (type === "seriesL" || type === "seriesC" || type === "seriesR") { + let dzx = 0, + dzr = 0; + if (type === "seriesL") dzx = (omega * value) / Z0; + else if (type === "seriesC") dzx = -1 / (omega * value * Z0); + else dzr = value / Z0; + + const arcPoints: GammaPoint[] = []; + if (type === "seriesR") { + for (let i = 0; i <= ARC_STEPS; i++) arcPoints.push(zToGamma(zr + (i / ARC_STEPS) * dzr, zx)); + } else { + for (let i = 0; i <= ARC_STEPS; i++) arcPoints.push(zToGamma(zr, zx + (i / ARC_STEPS) * dzx)); + } + return { toGamma: zToGamma(zr + dzr, zx + dzx), arcPoints }; + } + + // Shunt + let dby = 0, + dgy = 0; + if (type === "shuntL") dby = -Z0 / (omega * value); + else if (type === "shuntC") dby = omega * value * Z0; + else dgy = Z0 / value; + + const arcPoints: GammaPoint[] = []; + if (type === "shuntR") { + for (let i = 0; i <= ARC_STEPS; i++) arcPoints.push(yToGamma(gy + (i / ARC_STEPS) * dgy, by)); + } else { + for (let i = 0; i <= ARC_STEPS; i++) arcPoints.push(yToGamma(gy, by + (i / ARC_STEPS) * dby)); + } + return { toGamma: yToGamma(gy + dgy, by + dby), arcPoints }; +} + +function zToGamma(zr: number, zx: number): GammaPoint { + const d = (zr + 1) ** 2 + zx ** 2; + if (d < 1e-12) return { re: 1, im: 0 }; + return { re: (zr ** 2 + zx ** 2 - 1) / d, im: (2 * zx) / d }; +} + +function yToGamma(gy: number, by: number): GammaPoint { + const m2 = gy ** 2 + by ** 2; + if (m2 < 1e-12) return { re: 1, im: 0 }; + return zToGamma(gy / m2, -by / m2); +} + +// ── SmithChainAdapter ───────────────────────────────────────────────────────── + +export const CHAIN_COLOURS = ["#FF8800", "#FF4488", "#88FF00", "#00FFFF", "#FF0088", "#FFFF00"]; + +export class SmithChainAdapter { + private surface: SciChartSurface; + private wasmContext: TSciChart; + private stepSeries: Map = new Map(); + private dispatch: (a: SmithAction) => void = () => {}; + readonly Z0 = 50; + + constructor(surface: SciChartSurface, wasmContext: TSciChart) { + this.surface = surface; + this.wasmContext = wasmContext; + } + + setDispatch(d: (a: SmithAction) => void) { + this.dispatch = d; + } + + addStep(fromGamma: GammaPoint, type: ComponentType, value: number, frequency: number): void { + const { toGamma, arcPoints } = computeChainStep(fromGamma, type, value, frequency, this.Z0); + const step: ChainStep = { + id: `step-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, + type, + value, + frequency, + fromGamma, + toGamma, + arcPoints, + }; + this.dispatch({ type: "ADD_CHAIN_STEP", step }); + } + + update(state: SmithState): void { + for (const [id, { ds, rs }] of this.stepSeries) { + if (!state.chain.find((s) => s.id === id)) { + this.surface.renderableSeries.remove(rs); + ds.delete(); + this.stepSeries.delete(id); + } + } + state.chain.forEach((step, i) => { + if (!this.stepSeries.has(step.id)) { + const colour = CHAIN_COLOURS[i % CHAIN_COLOURS.length]; + const ds = new XyDataSeries(this.wasmContext); + const xArr = step.arcPoints.map((p) => p.re); + const yArr = step.arcPoints.map((p) => p.im); + ds.appendRange(xArr, yArr); + const rs = new FastLineRenderableSeries(this.wasmContext, { + dataSeries: ds, + stroke: colour, + strokeThickness: 2.5, + }); + this.surface.renderableSeries.add(rs); + this.stepSeries.set(step.id, { ds, rs }); + } + }); + } + + getChainTip(state: SmithState): GammaPoint | null { + if (state.chain.length > 0) return state.chain[state.chain.length - 1].toGamma; + const startMarker = state.markers.find((m) => m.isChainStart); + if (startMarker) return startMarker.gamma; + if (state.chainStartGamma) return state.chainStartGamma; + // Fall back: use the active marker as implicit chain start + const active = state.markers.find((m) => m.id === state.activeMarkerId); + return active?.gamma ?? null; + } +} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartGridCalculator.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartGridCalculator.ts new file mode 100644 index 000000000..a3e013b11 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartGridCalculator.ts @@ -0,0 +1,449 @@ +import { NumberRange } from "scichart/Core/NumberRange"; + +// ── Mutable runtime config (read by SmithGridCalculator.compute()) ──────────── + +export interface SmithGridConfig { + majorPxThreshold: number; // min pixel radius for a major circle/arc + minorPxThreshold: number; // min pixel radius for a minor circle/arc + targetTicks: number; // target major count (evenly spaced in s-space) + useCompactRange: boolean; // restrict lower R/G boundary to circles centred in viewport + maxTiers: number; // 0 = auto (nClip); >0 hard cap on tier iterations + minGapPx: number; // target pixel gap at convergence point; effective s-gap = minGapPx / pixPerUnit (auto-scales with zoom) + _version: number; // incremented on every change; drives stale-check invalidation +} + +export const smithGridConfig: SmithGridConfig = { + majorPxThreshold: 15, + minorPxThreshold: 5, + targetTicks: 8, + useCompactRange: true, + maxTiers: 0, + minGapPx: 3.6, + _version: 0, +}; + +/** Update one or more config fields and bump the version so caches invalidate. */ +export function updateSmithGridConfig(patch: Partial>): void { + Object.assign(smithGridConfig, patch); + smithGridConfig._version++; +} + +// ── Legacy constant exports (kept for external callers / tests) ─────────────── + +export const MAJOR_PX_THRESHOLD = 15; +export const MINOR_PX_THRESHOLD = 5; +export const TARGET_TICKS = 8; +export const GAP_ANGLE_RAD = (25 * Math.PI) / 180; +export const FALLBACK_TRIGGER = 4; + +// ── Curated candidates (44 values, RF engineering convention) ──────────────── + +export const CURATED_CANDIDATES = [ + 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.8, 2.0, 2.5, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, 9.0, 10, 12, 15, 20, 25, 30, 40, 50, 75, 100, 150, 200, +]; + +// ── Pure-math helpers (all exported for testing) ───────────────────────────── + +export function roundToNice(v: number): number { + if (v <= 0) return v; + const exp = Math.floor(Math.log10(v)); + const base = Math.pow(10, exp); + const mant = v / base; + let nice: number; + if (mant < 1.5) nice = 1; + else if (mant < 4.0) nice = 2; + else if (mant < 9.0) nice = 5; + else nice = 10; + return nice * base; +} + +export function pixelRadius(v: number, pixelPerUnit: number): number { + return pixelPerUnit / (v + 1); +} + +export function generateFallbackCandidates(xRange: NumberRange, pixelPerUnit: number): number[] { + const denomMax = 1 - xRange.max; + const rMax = denomMax > 0.001 ? Math.min((1 + xRange.max) / denomMax, 1000) : 1000; + const rPixelCutoff = Math.max(0.001, pixelPerUnit / MINOR_PX_THRESHOLD - 1); + if (rPixelCutoff >= rMax) return []; + + const sMin = 1 / (rMax + 1); + const sMax = 1 / (rPixelCutoff + 1); + const sStep = (sMax - sMin) / TARGET_TICKS; + if (sStep <= 0) return []; + + const seen = new Set(); + const result: number[] = []; + for (let s = sMin + sStep / 2; s < sMax; s += sStep) { + const r = 1 / s - 1; + const nice = roundToNice(r); + if (nice > 0 && !seen.has(nice)) { + seen.add(nice); + result.push(nice); + } + } + return result.sort((a, b) => a - b); +} + +export function buildCandidates(xRange: NumberRange, pixelPerUnit: number): number[] { + const curatedPassing = CURATED_CANDIDATES.filter((v) => pixelRadius(v, pixelPerUnit) >= MINOR_PX_THRESHOLD); + if (curatedPassing.length >= FALLBACK_TRIGGER) { + return [...CURATED_CANDIDATES]; + } + const fallback = generateFallbackCandidates(xRange, pixelPerUnit); + const seen = new Set(CURATED_CANDIDATES); + const combined = CURATED_CANDIDATES.slice(); + for (const v of fallback) { + if (!seen.has(v)) { + seen.add(v); + combined.push(v); + } + } + return combined.sort((a, b) => a - b); +} + +export function rCircleVisible(r: number, xRange: NumberRange, yRange: NumberRange): boolean { + const xLeft = (r - 1) / (r + 1); + const yHalf = 1 / (r + 1); + return xLeft <= xRange.max && 1 >= xRange.min && -yHalf <= yRange.max && yHalf >= yRange.min; +} + +export function gCircleVisible(g: number, xRange: NumberRange, yRange: NumberRange): boolean { + const xRight = (1 - g) / (1 + g); + const yHalf = 1 / (g + 1); + return -1 <= xRange.max && xRight >= xRange.min && -yHalf <= yRange.max && yHalf >= yRange.min; +} + +export function xArcVisible(absX: number, xRange: NumberRange, yRange: NumberRange): boolean { + const rad = 1 / absX; + return 1 - rad <= xRange.max && 1 + rad >= xRange.min && -2 * rad <= yRange.max && 2 * rad >= yRange.min; +} + +export function bArcVisible(absB: number, xRange: NumberRange, yRange: NumberRange): boolean { + const rad = 1 / absB; + return -1 - rad <= xRange.max && -1 + rad >= xRange.min && -2 * rad <= yRange.max && 2 * rad >= yRange.min; +} + +/** Kept as export for external callers; no longer used internally. */ +export function computeClip(v: number, maxOther: number): number { + const clip = (v + 1) / Math.tan(GAP_ANGLE_RAD / 2); + return Math.min(clip, maxOther); +} + +export function selectEvenlySpacedInS(candidates: number[], targetCount: number): number[] { + if (candidates.length === 0) return []; + if (candidates.length <= targetCount) return [...candidates]; + + const sValues = candidates.map((v) => 1 / (v + 1)); + const sMin = Math.min(...sValues); + const sMax = Math.max(...sValues); + const step = (sMax - sMin) / (targetCount - 1); + + const selected: number[] = []; + for (let i = 0; i < targetCount; i++) { + const sTarget = sMin + i * step; + let best = candidates[0]; + let bestDist = Math.abs(sValues[0] - sTarget); + for (let j = 1; j < candidates.length; j++) { + const d = Math.abs(sValues[j] - sTarget); + if (d < bestDist) { + bestDist = d; + best = candidates[j]; + } + } + if (!selected.includes(best)) selected.push(best); + } + return selected.sort((a, b) => a - b); +} + +// ── Viewport-derived visible-value max ────────────────────────────────────── + +function visibleValueMax(xRange: NumberRange, isAdmittance: boolean, isXFamily: boolean): number { + if (!isAdmittance && !isXFamily) { + const d = 1 - xRange.max; + return d > 0.001 ? (1 + xRange.max) / d : Infinity; + } + if (isAdmittance && !isXFamily) { + const d = 1 + xRange.min; + return d > 0.001 ? Math.max(0, (1 - xRange.min) / d) : Infinity; + } + if (!isAdmittance && isXFamily) { + const d = 1 - xRange.max; + return d > 0.001 ? 1 / d : Infinity; + } + const d = 1 + xRange.min; + return d > 0.001 ? 1 / d : Infinity; +} + +function syntheticMajorsForRange( + visibleMax: number, + targetCount: number, + pixPerUnit: number, + majorPxThreshold: number +): number[] { + if (visibleMax <= 0 || !isFinite(visibleMax)) return []; + const sMin = 1 / (visibleMax + 1); + const sMax = 1.0; + const step = (sMax - sMin) / targetCount; + if (step <= 0) return []; + const seen = new Set(); + const result: number[] = []; + for (let i = 0; i < targetCount; i++) { + const s = sMin + (i + 0.5) * step; + const v = 1 / s - 1; + const nice = roundToNice(v); + if ( + nice > 0 && + nice <= visibleMax * 1.1 && + !seen.has(nice) && + pixelRadius(nice, pixPerUnit) >= majorPxThreshold + ) { + seen.add(nice); + result.push(nice); + } + } + return result.sort((a, b) => a - b); +} + +// ── Compact lower boundary ─────────────────────────────────────────────────── + +function compactLowerBound( + xRange: NumberRange, + _yRange: NumberRange, + isAdmittance: boolean, + isXFamily: boolean +): number { + if (!smithGridConfig.useCompactRange || isXFamily) return 0; + if (!isAdmittance) { + const d = 1 - xRange.min; + return d > 0.001 ? Math.max(0, xRange.min / d) : 0; + } else { + const d = 1 + xRange.max; + return d > 0.001 ? Math.max(0, -xRange.max / d) : 0; + } +} + +// ── Tiered minor tick computation ──────────────────────────────────────────── + +function computeTieredMinors( + candidates: number[], + valueMajorsSorted: number[], + pixPerUnit: number, + xRange: NumberRange, + yRange: NumberRange, + isAdmittance: boolean, + isXFamily: boolean, + otherFamilyMajors: number[] = [] +): number[] { + const result: number[] = []; + if (valueMajorsSorted.length === 0) return result; + + const cfg = smithGridConfig; + const addedSet = new Set(valueMajorsSorted); + + const compactLo = compactLowerBound(xRange, yRange, isAdmittance, isXFamily); + + let levelBoundaries = [compactLo, ...valueMajorsSorted.filter((v) => v > compactLo)]; + + // maxTiers: default 8 (minGap check limits actual depth at any given zoom level). + const maxTiers = cfg.maxTiers > 0 ? cfg.maxTiers : 8; + + // Dynamic gap threshold: cfg.minGapPx is a target pixel gap at the convergence point. + // Dividing by pixPerUnit converts to s-space, so it automatically tightens as you zoom in. + const effectiveMinGap = cfg.minGapPx / pixPerUnit; + + for (let tier = 1; tier <= maxTiers; tier++) { + const tierValues: number[] = []; + + for (let i = 0; i + 1 < levelBoundaries.length; i++) { + const lo = levelBoundaries[i]; + const hi = levelBoundaries[i + 1]; + + // Gap in s-space at the convergence end; compared against effectiveMinGap. + const pixGap = 1 / Math.sqrt(lo + 1) - 1 / Math.sqrt(hi + 1); + if (pixGap < effectiveMinGap) continue; + + const sLo = 1 / (hi + 1); + const sHi = 1 / (lo + 1); + const sMid = (sLo + sHi) / 2; + const vIdeal = 1 / sMid - 1; + + const inGap = candidates.filter( + (v) => v > lo && v < hi && !addedSet.has(v) && pixelRadius(v, pixPerUnit) >= cfg.minorPxThreshold + ); + + let vBest: number | null = null; + if (inGap.length > 0) { + let bestDist = Infinity; + for (const v of inGap) { + const d = Math.abs(1 / (v + 1) - sMid); + if (d < bestDist) { + bestDist = d; + vBest = v; + } + } + } else { + if (vIdeal > lo && vIdeal < hi && pixelRadius(vIdeal, pixPerUnit) >= cfg.minorPxThreshold) { + vBest = vIdeal; + } + } + + if (vBest === null || addedSet.has(vBest)) continue; + + const isVisible = isXFamily + ? isAdmittance + ? bArcVisible(vBest, xRange, yRange) + : xArcVisible(vBest, xRange, yRange) + : isAdmittance + ? gCircleVisible(vBest, xRange, yRange) + : rCircleVisible(vBest, xRange, yRange); + if (!isVisible) continue; + + // Intrinsic clip gives a universal arc angle per tier, independent of viewport. + // Snapping to the nearest other-family major ensures the arc endpoint lands + // on a visible gridline intersection rather than floating mid-chart. + const intrinsicClip = (vBest + 1) * Math.pow(2, 3 - tier); + const anchor = otherFamilyMajors.find((a) => a >= intrinsicClip); + const clip = anchor !== undefined ? anchor : intrinsicClip; + result.push(vBest, clip); + tierValues.push(vBest); + addedSet.add(vBest); + } + + if (tierValues.length === 0) break; + levelBoundaries = [...levelBoundaries, ...tierValues].sort((a, b) => a - b); + } + + return result; +} + +// ── SmithGridCalculator ────────────────────────────────────────────────────── + +export class SmithGridCalculator { + private readonly _variant: "impedance" | "admittance"; + + private _lastXMin = NaN; + private _lastXMax = NaN; + private _lastYMin = NaN; + private _lastYMax = NaN; + private _lastPixPerUnit = NaN; + private _lastVersion = -1; + + private _rMajor: number[] = []; + private _rMinor: number[] = []; + private _xMajor: number[] = []; + private _xMinor: number[] = []; + + constructor(variant: "impedance" | "admittance") { + this._variant = variant; + } + + get rMajor(): number[] { + return this._rMajor; + } + get rMinor(): number[] { + return this._rMinor; + } + get xMajor(): number[] { + return this._xMajor; + } + get xMinor(): number[] { + return this._xMinor; + } + + compute( + xRange: NumberRange, + yRange: NumberRange, + xCoordCalc: { getCoordWidth(v: number): number }, + _yCoordCalc: { getCoordWidth(v: number): number } + ): void { + const pixPerUnit = Math.abs(xCoordCalc.getCoordWidth(1)); + const cfg = smithGridConfig; + + if ( + this._lastXMin === xRange.min && + this._lastXMax === xRange.max && + this._lastYMin === yRange.min && + this._lastYMax === yRange.max && + this._lastPixPerUnit === pixPerUnit && + this._lastVersion === cfg._version + ) + return; + + this._lastXMin = xRange.min; + this._lastXMax = xRange.max; + this._lastYMin = yRange.min; + this._lastYMax = yRange.max; + this._lastPixPerUnit = pixPerUnit; + this._lastVersion = cfg._version; + + const isAdmittance = this._variant === "admittance"; + const candRange = isAdmittance ? new NumberRange(-xRange.max, -xRange.min) : xRange; + const candidates = buildCandidates(candRange, pixPerUnit); + + // ── Majors ──────────────────────────────────────────────────────────── + let rMajorAll = candidates.filter((v) => { + if (pixelRadius(v, pixPerUnit) < cfg.majorPxThreshold) return false; + return isAdmittance ? gCircleVisible(v, xRange, yRange) : rCircleVisible(v, xRange, yRange); + }); + if (rMajorAll.length < cfg.targetTicks) { + const visMax = visibleValueMax(xRange, isAdmittance, false); + const synthetic = syntheticMajorsForRange(visMax, cfg.targetTicks, pixPerUnit, cfg.majorPxThreshold); + const rSet = new Set(rMajorAll); + for (const v of synthetic) { + const isVis = isAdmittance ? gCircleVisible(v, xRange, yRange) : rCircleVisible(v, xRange, yRange); + if (!rSet.has(v) && isVis) { + rSet.add(v); + rMajorAll.push(v); + } + } + rMajorAll.sort((a, b) => a - b); + } + this._rMajor = selectEvenlySpacedInS(rMajorAll, cfg.targetTicks); + + let xMajorAll = candidates.filter((v) => { + if (pixelRadius(v, pixPerUnit) < cfg.majorPxThreshold) return false; + return isAdmittance ? bArcVisible(v, xRange, yRange) : xArcVisible(v, xRange, yRange); + }); + if (xMajorAll.length < cfg.targetTicks) { + const visMax = visibleValueMax(xRange, isAdmittance, true); + const synthetic = syntheticMajorsForRange(visMax, cfg.targetTicks, pixPerUnit, cfg.majorPxThreshold); + const xSet = new Set(xMajorAll); + for (const v of synthetic) { + const isVis = isAdmittance ? bArcVisible(v, xRange, yRange) : xArcVisible(v, xRange, yRange); + if (!xSet.has(v) && isVis) { + xSet.add(v); + xMajorAll.push(v); + } + } + xMajorAll.sort((a, b) => a - b); + } + this._xMajor = selectEvenlySpacedInS(xMajorAll, cfg.targetTicks); + + const rMajorSorted = [...this._rMajor].sort((a, b) => a - b); + const xMajorSorted = [...this._xMajor].sort((a, b) => a - b); + + // ── Tiered minors ────────────────────────────────────────────────────── + this._rMinor = computeTieredMinors( + candidates, + rMajorSorted, + pixPerUnit, + xRange, + yRange, + isAdmittance, + false, + xMajorSorted + ); + this._xMinor = computeTieredMinors( + candidates, + xMajorSorted, + pixPerUnit, + xRange, + yRange, + isAdmittance, + true, + rMajorSorted + ); + } +} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartMarkers.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartMarkers.ts new file mode 100644 index 000000000..984357036 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartMarkers.ts @@ -0,0 +1,577 @@ +import { + SciChartSurface, + TSciChart, + CustomAnnotation, + LineAnnotation, + TextAnnotation, + XyDataSeries, + FastLineRenderableSeries, + ECoordinateMode, + EHorizontalAnchorPoint, + EVerticalAnchorPoint, + ChartModifierBase2D, + ModifierMouseArgs, + EExecuteOn, +} from "scichart"; +import { GammaPoint, SmithState, SmithAction, Marker, DragMode } from "./useSmithChart"; +import { SmithChartResistanceAxis } from "./smithChartAxes"; + +export type MarkerReadout = { + gammaMag: number; + gammaAngleDeg: number; + zr: number; + zx: number; + gy: number; + by: number; + vswr: number; + returnLoss: number; + mismatchLoss: number; + q: number; + wtg: number; + wtl: number; +}; + +export function computeReadouts(gamma: GammaPoint): MarkerReadout { + const { re, im } = gamma; + const gammaMag = Math.sqrt(re * re + im * im); + const gammaAngleDeg = (Math.atan2(im, re) * 180) / Math.PI; + + // Z = (1+Γ)/(1−Γ) + const denom = (1 - re) * (1 - re) + im * im; + const zr = denom > 1e-10 ? (1 - re * re - im * im) / denom : Infinity; + const zx = denom > 1e-10 ? (2 * im) / denom : Infinity; + + // Y = 1/Z + const zMagSq = zr * zr + zx * zx; + const gy = zMagSq > 1e-10 ? zr / zMagSq : Infinity; + const by = zMagSq > 1e-10 ? -zx / zMagSq : Infinity; + + const vswr = gammaMag < 1 - 1e-10 ? (1 + gammaMag) / (1 - gammaMag) : Infinity; + const returnLoss = gammaMag > 1e-10 ? -20 * Math.log10(gammaMag) : Infinity; + const mismatchLoss = gammaMag < 1 - 1e-10 ? -10 * Math.log10(1 - gammaMag * gammaMag) : Infinity; + const q = isFinite(zr) && zr > 1e-10 ? Math.abs(zx) / zr : Infinity; + + // WTG: (π − ∠Γ_rad) / (4π) mod 0.5 + const gammaAngleRad = Math.atan2(im, re); + let wtg = ((Math.PI - gammaAngleRad) / (4 * Math.PI)) % 0.5; + if (wtg < 0) wtg += 0.5; + const wtl = 0.5 - wtg; + + return { + gammaMag, + gammaAngleDeg, + zr, + zx, + gy, + by, + vswr, + returnLoss, + mismatchLoss, + q, + wtg, + wtl, + }; +} + +// ── Highlight arc helpers (also used by drawExample.ts) ─────────────────────── + +export function populateRCircle(ds: XyDataSeries, r: number) { + ds.clear(); + if (!isFinite(r) || r < 0) return; + const cx = r / (1 + r); + const rad = 1 / (1 + r); + const n = 200; + const xArr = new Float64Array(n + 1); + const yArr = new Float64Array(n + 1); + for (let i = 0; i <= n; i++) { + const angle = (i / n) * 2 * Math.PI; + xArr[i] = cx + rad * Math.cos(angle); + yArr[i] = rad * Math.sin(angle); + } + ds.appendRange(xArr, yArr); +} + +export function populateXArc(ds: XyDataSeries, xVal: number) { + ds.clear(); + if (!isFinite(xVal)) return; + + if (Math.abs(xVal) < 0.001) { + ds.appendRange([-1, 1], [0, 0]); + return; + } + + const absX = Math.abs(xVal); + const isPos = xVal > 0; + const radius = 1 / absX; + const cx = 1; + const cy = isPos ? radius : -radius; + + const xv2 = absX * absX; + const xInt = (xv2 - 1) / (1 + xv2); + const yInt = isPos ? (2 * absX) / (1 + xv2) : (-2 * absX) / (1 + xv2); + + const thetaOther = Math.atan2(yInt - cy, xInt - cx); + const thetaOrigin = isPos ? -Math.PI / 2 : Math.PI / 2; + + let startAngle: number; + let endAngle: number; + + if (isPos) { + startAngle = thetaOther; + endAngle = thetaOrigin; + while (endAngle <= startAngle) endAngle += 2 * Math.PI; + } else { + startAngle = thetaOrigin; + endAngle = thetaOther; + while (endAngle <= startAngle) endAngle += 2 * Math.PI; + } + + const n = 200; + const xArr = new Float64Array(n + 1); + const yArr = new Float64Array(n + 1); + for (let i = 0; i <= n; i++) { + const angle = startAngle + (i / n) * (endAngle - startAngle); + xArr[i] = cx + radius * Math.cos(angle); + yArr[i] = cy + radius * Math.sin(angle); + } + ds.appendRange(xArr, yArr); +} + +export function populateCircle(ds: XyDataSeries, cx: number, cy: number, radius: number) { + ds.clear(); + if (radius < 0.001) return; + const n = 200; + const xArr = new Float64Array(n + 1); + const yArr = new Float64Array(n + 1); + for (let i = 0; i <= n; i++) { + const angle = (i / n) * 2 * Math.PI; + xArr[i] = cx + radius * Math.cos(angle); + yArr[i] = cy + radius * Math.sin(angle); + } + ds.appendRange(xArr, yArr); +} + +// ── Marker colours ──────────────────────────────────────────────────────────── + +export const MARKER_COLOURS = ["#FF4444", "#44AAFF", "#FFAA00", "#44FF88", "#FF44CC", "#88FF44"]; + +// ── Annotation IDs helpers ──────────────────────────────────────────────────── + +function dotAnnotationId(markerId: string) { + return `dot-${markerId}`; +} + +function spokeLineId(markerId: string) { + return `spoke-line-${markerId}`; +} + +function spokeTextId(markerId: string) { + return `spoke-text-${markerId}`; +} + +// ── CustomAnnotation SVG for a coloured dot ─────────────────────────────────── + +function makeDotSvg(color: string): string { + return ``; +} + +// ── Constrained drag helpers ────────────────────────────────────────────────── + +function projectToCircle(px: number, py: number, cx: number, cy: number, radius: number): GammaPoint { + const dx = px - cx; + const dy = py - cy; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < 1e-10) return { re: cx + radius, im: cy }; + return { re: cx + (dx / dist) * radius, im: cy + (dy / dist) * radius }; +} + +interface DragStart { + gammaMag: number; + r: number; + x: number; + g: number; + b: number; +} + +function applyConstraint(re: number, im: number, mode: DragMode, start: DragStart): GammaPoint { + switch (mode) { + case "gamma": { + const mag = Math.sqrt(re * re + im * im); + if (mag < 1e-10) return { re: start.gammaMag, im: 0 }; + return { re: (re / mag) * start.gammaMag, im: (im / mag) * start.gammaMag }; + } + case "R": { + const cx = start.r / (1 + start.r); + return projectToCircle(re, im, cx, 0, 1 / (1 + start.r)); + } + case "X": { + const absX = Math.abs(start.x); + if (absX < 0.001) return { re, im: 0 }; + const cy = start.x > 0 ? 1 / absX : -1 / absX; + return projectToCircle(re, im, 1, cy, 1 / absX); + } + case "G": { + const cx = -(start.g / (1 + start.g)); + return projectToCircle(re, im, cx, 0, 1 / (1 + start.g)); + } + case "B": { + const absB = Math.abs(start.b); + if (absB < 0.001) return { re, im: 0 }; + const cy = start.b > 0 ? 1 / absB : -1 / absB; + return projectToCircle(re, im, -1, cy, 1 / absB); + } + default: + return { re, im }; + } +} + +// ── SmithClickModifier ──────────────────────────────────────────────────────── + +class SmithClickModifier extends ChartModifierBase2D { + readonly type = "SmithClickModifier"; + + private dispatch: ((a: SmithAction) => void) | null = null; + private markerPositions: Map = new Map(); + private dragTargetId: string | null = null; + + // Captured at drag start for constraint projection + private dragStart: DragStart = { gammaMag: 0, r: 0, x: 0, g: 0, b: 0 }; + private dragStartMode: DragMode = "free"; + + setDispatch(d: (a: SmithAction) => void) { + this.dispatch = d; + } + + setMarkerPositions(positions: Map) { + this.markerPositions = positions; + } + + modifierMouseDown(args: ModifierMouseArgs) { + super.modifierMouseDown(args); + this.dragTargetId = null; + if (!this.checkExecuteConditions(args).isPrimary) return; + + const hitId = this.hitTestMarkers(args.mousePoint.x, args.mousePoint.y); + if (hitId !== null) { + this.dragTargetId = hitId; + this.dispatch?.({ type: "SET_ACTIVE_MARKER", id: hitId }); + + const md = this.markerPositions.get(hitId); + if (md) { + this.dragStartMode = md.dragMode; + const { re: mr, im: mi } = md; + const dz = (1 - mr) ** 2 + mi ** 2; + const zr = dz > 1e-10 ? (1 - mr * mr - mi * mi) / dz : 1e6; + const zx = dz > 1e-10 ? (2 * mi) / dz : 0; + const zm2 = zr ** 2 + zx ** 2; + this.dragStart = { + gammaMag: Math.sqrt(mr * mr + mi * mi), + r: zr, + x: zx, + g: zm2 > 1e-10 ? zr / zm2 : 1e6, + b: zm2 > 1e-10 ? -zx / zm2 : 0, + }; + } else { + this.dragStartMode = "free"; + } + + args.handled = true; + return; + } + + const { re, im } = this.pixelToData(args.mousePoint.x, args.mousePoint.y); + if (re * re + im * im <= 1) { + this.dispatch?.({ type: "ADD_MARKER", gamma: { re, im } }); + args.handled = true; + } + } + + modifierMouseMove(args: ModifierMouseArgs) { + super.modifierMouseMove(args); + if (this.dragTargetId !== null) { + let { re, im } = this.pixelToData(args.mousePoint.x, args.mousePoint.y); + + if (this.dragStartMode !== "free") { + const p = applyConstraint(re, im, this.dragStartMode, this.dragStart); + re = p.re; + im = p.im; + } + + // Clamp to unit circle + const mag = Math.sqrt(re * re + im * im); + if (mag > 0.999) { + re = (re / mag) * 0.999; + im = (im / mag) * 0.999; + } + + this.dispatch?.({ type: "MOVE_MARKER", id: this.dragTargetId, gamma: { re, im } }); + args.handled = true; + } + } + + modifierMouseUp(args: ModifierMouseArgs) { + super.modifierMouseUp(args); + this.dragTargetId = null; + } + + private pixelToData(px: number, py: number): { re: number; im: number } { + const xCalc = this.parentSurface.xAxes.get(0).getCurrentCoordinateCalculator(); + const yCalc = this.parentSurface.yAxes.get(0).getCurrentCoordinateCalculator(); + const svr = this.parentSurface.seriesViewRect; + return { re: xCalc.getDataValue(px - svr.left), im: yCalc.getDataValue(py - svr.top) }; + } + + private hitTestMarkers(px: number, py: number): string | null { + const RADIUS_PX = 12; + const xCalc = this.parentSurface.xAxes.get(0).getCurrentCoordinateCalculator(); + const yCalc = this.parentSurface.yAxes.get(0).getCurrentCoordinateCalculator(); + const svr = this.parentSurface.seriesViewRect; + const localX = px - svr.left; + const localY = py - svr.top; + for (const [id, pos] of this.markerPositions) { + const sx = xCalc.getCoordinate(pos.re); + const sy = yCalc.getCoordinate(pos.im); + const dx = localX - sx; + const dy = localY - sy; + if (dx * dx + dy * dy <= RADIUS_PX * RADIUS_PX) return id; + } + return null; + } +} + +// ── SmithMarkersAdapter ─────────────────────────────────────────────────────── + +export class SmithMarkersAdapter { + private surface: SciChartSurface; + private wasmContext: TSciChart; + private modifier: SmithClickModifier; + + private rCircleDS: XyDataSeries; + private xArcDS: XyDataSeries; + private gammaCircleDS: XyDataSeries; + + // Track annotation IDs currently on the chart + private activeAnnotationIds = new Set(); + + constructor(surface: SciChartSurface, wasmContext: TSciChart) { + this.surface = surface; + this.wasmContext = wasmContext; + + // ── Highlight series ────────────────────────────────────────────────── + this.rCircleDS = new XyDataSeries(wasmContext); + surface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: this.rCircleDS, + stroke: "#FF4444", + strokeThickness: 2.5, + }) + ); + + this.xArcDS = new XyDataSeries(wasmContext); + surface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: this.xArcDS, + stroke: "#4488FF", + strokeThickness: 2.5, + }) + ); + + this.gammaCircleDS = new XyDataSeries(wasmContext); + surface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: this.gammaCircleDS, + stroke: "#44CC44", + strokeThickness: 1.5, + strokeDashArray: [5, 5], + }) + ); + + // ── Modifier ────────────────────────────────────────────────────────── + this.modifier = new SmithClickModifier({ + executeCondition: { button: EExecuteOn.MouseLeftButton }, + }); + surface.chartModifiers.add(this.modifier); + } + + setDispatch(d: (a: SmithAction) => void) { + this.modifier.setDispatch(d); + } + + update(state: SmithState) { + this.syncMarkerAnnotations(state.markers); + this.updateHighlights(state); + } + + // ── Sync annotations ────────────────────────────────────────────────────── + + private syncMarkerAnnotations(markers: Marker[]) { + const desiredIds = new Set(); + const markerPositions = new Map(); + + for (let i = 0; i < markers.length; i++) { + const marker = markers[i]; + const color = MARKER_COLOURS[i % MARKER_COLOURS.length]; + markerPositions.set(marker.id, { re: marker.gamma.re, im: marker.gamma.im, dragMode: marker.dragMode }); + + // Dot + const dotId = dotAnnotationId(marker.id); + desiredIds.add(dotId); + if (!this.activeAnnotationIds.has(dotId)) { + this.surface.annotations.add(this.makeDotAnnotation(dotId, marker, color)); + this.activeAnnotationIds.add(dotId); + } else { + this.moveDotAnnotation(dotId, marker); + } + + // Spoke line + text + const lineId = spokeLineId(marker.id); + const textId = spokeTextId(marker.id); + desiredIds.add(lineId); + desiredIds.add(textId); + if (!this.activeAnnotationIds.has(lineId)) { + this.surface.annotations.add(this.makeSpokeLineAnnotation(lineId, marker, color)); + this.activeAnnotationIds.add(lineId); + } else { + this.moveSpokeLineAnnotation(lineId, marker); + } + if (!this.activeAnnotationIds.has(textId)) { + this.surface.annotations.add(this.makeSpokeTextAnnotation(textId, marker, color)); + this.activeAnnotationIds.add(textId); + } else { + this.moveSpokeTextAnnotation(textId, marker); + } + } + + // Remove stale annotations + for (const id of [...this.activeAnnotationIds]) { + if (!desiredIds.has(id)) { + const ann = this.surface.annotations.getById(id); + if (ann) { + this.surface.annotations.remove(ann); + } + this.activeAnnotationIds.delete(id); + } + } + + this.modifier.setMarkerPositions(markerPositions); + } + + private makeDotAnnotation(id: string, marker: Marker, color: string): CustomAnnotation { + return new CustomAnnotation({ + id, + x1: marker.gamma.re, + y1: marker.gamma.im, + xCoordinateMode: ECoordinateMode.DataValue, + yCoordinateMode: ECoordinateMode.DataValue, + svgString: makeDotSvg(color), + }); + } + + private moveDotAnnotation(id: string, marker: Marker) { + const ann = this.surface.annotations.getById(id); + if (ann) { + ann.x1 = marker.gamma.re; + ann.y1 = marker.gamma.im; + } + } + + private spokeRimPoint(marker: Marker): { rimRe: number; rimIm: number; theta: number } { + const theta = Math.atan2(marker.gamma.im, marker.gamma.re); + return { rimRe: Math.cos(theta) * 1.08, rimIm: Math.sin(theta) * 1.08, theta }; + } + + private makeSpokeLineAnnotation(id: string, marker: Marker, color: string): LineAnnotation { + const { rimRe, rimIm } = this.spokeRimPoint(marker); + return new LineAnnotation({ + id, + x1: marker.gamma.re, + y1: marker.gamma.im, + x2: rimRe, + y2: rimIm, + xCoordinateMode: ECoordinateMode.DataValue, + yCoordinateMode: ECoordinateMode.DataValue, + stroke: color, + strokeThickness: 1, + }); + } + + private moveSpokeLineAnnotation(id: string, marker: Marker) { + const ann = this.surface.annotations.getById(id) as LineAnnotation | undefined; + if (ann) { + const { rimRe, rimIm } = this.spokeRimPoint(marker); + ann.x1 = marker.gamma.re; + ann.y1 = marker.gamma.im; + ann.x2 = rimRe; + ann.y2 = rimIm; + } + } + + private spokeText(marker: Marker): string { + const angleDeg = (Math.atan2(marker.gamma.im, marker.gamma.re) * 180) / Math.PI; + return `${marker.label} ${angleDeg.toFixed(1)}°`; + } + + private makeSpokeTextAnnotation(id: string, marker: Marker, color: string): TextAnnotation { + const { rimRe, rimIm } = this.spokeRimPoint(marker); + const labelRe = Math.cos(Math.atan2(rimIm, rimRe)) * 1.14; + const labelIm = Math.sin(Math.atan2(rimIm, rimRe)) * 1.14; + return new TextAnnotation({ + id, + x1: labelRe, + y1: labelIm, + xCoordinateMode: ECoordinateMode.DataValue, + yCoordinateMode: ECoordinateMode.DataValue, + text: this.spokeText(marker), + fontSize: 10, + fontFamily: "monospace", + textColor: color, + horizontalAnchorPoint: EHorizontalAnchorPoint.Center, + verticalAnchorPoint: EVerticalAnchorPoint.Center, + }); + } + + private moveSpokeTextAnnotation(id: string, marker: Marker) { + const ann = this.surface.annotations.getById(id) as TextAnnotation | undefined; + if (ann) { + const theta = Math.atan2(marker.gamma.im, marker.gamma.re); + ann.x1 = Math.cos(theta) * 1.14; + ann.y1 = Math.sin(theta) * 1.14; + ann.text = this.spokeText(marker); + } + } + + // ── Update highlight arcs ───────────────────────────────────────────────── + + private updateHighlights(state: SmithState) { + const rAxis = this.surface.xAxes.get(0) as SmithChartResistanceAxis; + if (!state.activeMarkerId) { + this.rCircleDS.clear(); + this.xArcDS.clear(); + this.gammaCircleDS.clear(); + rAxis.selectedIntersection = null; + return; + } + const active = state.markers.find((m) => m.id === state.activeMarkerId); + if (!active) { + this.rCircleDS.clear(); + this.xArcDS.clear(); + this.gammaCircleDS.clear(); + rAxis.selectedIntersection = null; + return; + } + + const { re, im } = active.gamma; + const denom = (1 - re) * (1 - re) + im * im; + const r = denom > 1e-10 ? (1 - re * re - im * im) / denom : Infinity; + const x = denom > 1e-10 ? (2 * im) / denom : Infinity; + const gammaMag = Math.sqrt(re * re + im * im); + + populateRCircle(this.rCircleDS, r); + populateXArc(this.xArcDS, x); + populateCircle(this.gammaCircleDS, 0, 0, gammaMag); + + // Drive intersection labels on the axis renderers — use exact marker R/X + // so labels align with the visual highlighted arcs (not snapped major values). + rAxis.selectedIntersection = isFinite(r) && r >= 0 && isFinite(x) ? { r, x } : null; + } +} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartRim.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartRim.ts new file mode 100644 index 000000000..5e1815504 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartRim.ts @@ -0,0 +1,96 @@ +import { TSciChart, Rect } from "scichart"; +import { WebGlRenderContext2D, ELineDrawMode } from "scichart/Charting/Drawing/WebGlRenderContext2D"; +import { SCRTPen } from "scichart/types/TSciChart"; +import { getVectorColorVertex, getVertex } from "scichart/Charting/Visuals/Helpers/NativeObject"; + +export const DEFAULT_RIM_TICK_SIZE = 8; // pixels — major tick length +export const DEFAULT_RIM_LABEL_OFFSET = 2; // pixels — gap from tick tip to label anchor +const DEFAULT_RIM_MAJOR_TICK_STEP = 30; // degrees +const DEFAULT_RIM_MINOR_TICK_STEP = 10; // degrees + +/** + * Angular spacing and label-gap configuration for the angle-of-Γ rim ring. + * + * Inner/outer radii are not configurable here — they are derived at render time + * from the rim-line stroke thickness and the tick-line tickSize so ticks always + * sit flush against the outer edge of the unit-circle boundary. + * + * majorTickStep must be an integer multiple of minorTickStep. + */ +export type RimConfig = { + /** Angular spacing in degrees between major (labelled) ticks. Default 30. */ + majorTickStep?: number; + /** Angular spacing in degrees between minor ticks. Default 10. */ + minorTickStep?: number; + /** + * Pixel gap between the outer tip of a major tick and the label anchor. + * Must be > 0; values ≤ 0 are clamped to 1. Default 2. + */ + labelOffset?: number; +}; + +/** + * Draws angle-of-Γ tick marks around the rim (just outside the unit circle). + * Called from SmithChartResistanceAxis.drawGridLines on the major pass. + * Labels are emitted via the drawLabel callback so the axis renderer can draw + * them using its own text-rendering pipeline. + * + * @param tickSizePx Major tick length in pixels; minor ticks use half. + * @param rimLineHalfThicknessPx Half the rendered stroke-width of the unit-circle + * line, so tick starts are flush with its outer edge (pass 0 if no rim line). + */ +export function drawRimTicks( + renderContext: WebGlRenderContext2D, + xCalc: any, + yCalc: any, + wasmContext: TSciChart, + pen: SCRTPen, + clipRect: Rect, + leftPad: number, + topPad: number, + config: RimConfig, + tickSizePx: number, + rimLineHalfThicknessPx: number, + drawLabel: (text: string, px: number, py: number, angleDeg: number) => void +): void { + const majorStep = config.majorTickStep ?? DEFAULT_RIM_MAJOR_TICK_STEP; + const minorStep = config.minorTickStep ?? DEFAULT_RIM_MINOR_TICK_STEP; + const labelOffsetPx = Math.max(1, config.labelOffset ?? DEFAULT_RIM_LABEL_OFFSET); + + // Convert pixel measurements to data units at the current zoom level. + // getCoordWidth(1) = pixels per 1 data unit on the x axis. + const pxPerDataUnit = Math.abs(xCalc.getCoordWidth(1)); + const innerR = 1 + rimLineHalfThicknessPx / pxPerDataUnit; + const majorOuterR = innerR + tickSizePx / pxPerDataUnit; + const minorOuterR = innerR + tickSizePx / 2 / pxPerDataUnit; + const labelR = majorOuterR + labelOffsetPx / pxPerDataUnit; + + const vertices = getVectorColorVertex(wasmContext); + const vertex = getVertex(wasmContext, 0, 0); + + for (let deg = 0; deg < 360; deg += minorStep) { + const isMajorTick = deg % majorStep === 0; + const rad = (deg * Math.PI) / 180; + const cosA = Math.cos(rad); + const sinA = Math.sin(rad); + + const tickOuterR = isMajorTick ? majorOuterR : minorOuterR; + + vertex.SetPosition(xCalc.getCoordinate(cosA * innerR), yCalc.getCoordinate(sinA * innerR)); + vertices.push_back(vertex); + vertex.SetPosition(xCalc.getCoordinate(cosA * tickOuterR), yCalc.getCoordinate(sinA * tickOuterR)); + vertices.push_back(vertex); + + if (isMajorTick) { + // Convert to standard ∠Γ convention: 0° = right, CCW positive, range ±180° + const labelAngle = deg > 180 ? deg - 360 : deg; + const lx = xCalc.getCoordinate(cosA * labelR); + const ly = yCalc.getCoordinate(sinA * labelR); + drawLabel(`${labelAngle}°`, lx, ly, deg); + } + } + + if (vertices.size() > 0) { + renderContext.drawLinesNative(vertices, pen, ELineDrawMode.DiscontinuousLine, clipRect, leftPad, topPad); + } +} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartScenarios.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartScenarios.ts new file mode 100644 index 000000000..c60b2e84b --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartScenarios.ts @@ -0,0 +1,160 @@ +import { SmithAction, GammaPoint, ComponentType } from "./useSmithChart"; + +export type ScenarioStep = { + description: string; + run: ( + dispatch: (a: SmithAction) => void, + addChainStep: (from: GammaPoint, type: ComponentType, value: number, freq: number) => void + ) => void; +}; + +export type Scenario = { + id: string; + title: string; + summary: string; + steps: ScenarioStep[]; +}; + +export const SCENARIOS: Scenario[] = [ + { + id: "tl-analysis", + title: "Transmission Line Analysis", + summary: "How impedance transforms along a lossless 50\u03a9 line.", + steps: [ + { + description: + "Place load Z\u2097 = 100+j50 \u03a9 (\u0393 = 0.4+j0.2). " + + "The marker sits in the upper-right \u2014 a 2:1 inductive mismatch. " + + "The VSWR circle is set to match |\u0393| to show the rotation arc.", + run: (dispatch) => { + dispatch({ type: "SET_FREQUENCY", frequency: 1e9 }); + dispatch({ type: "ADD_MARKER", gamma: { re: 0.4, im: 0.2 } }); + dispatch({ type: "SET_VSWR", vswr: 2.618 }); + }, + }, + { + description: + "Add a 0.15\u03bb lossless transmission line. The impedance rotates " + + "clockwise around the chart centre on the constant-|\u0393| circle \u2014 " + + "this is the fundamental transmission-line rule.", + run: (_dispatch, addChainStep) => { + addChainStep({ re: 0.4, im: 0.2 }, "TL", 0.15, 1e9); + }, + }, + { + description: + "Marker M2 is placed at the new impedance point (\u0393 \u2248 0.07\u2212j0.44). " + + "Expand M2 to read Z, VSWR and WTG. The WTG value (outer rim) " + + "confirms the 0.15\u03bb rotation toward the generator.", + run: (dispatch) => { + dispatch({ type: "ADD_MARKER", gamma: { re: 0.0667, im: -0.4422 } }); + }, + }, + ], + }, + { + id: "single-stub", + title: "Single-Stub Matching", + summary: "Match a resistive mismatch using a shunt stub.", + steps: [ + { + description: + "Place load Z\u2097 = 100+j0 \u03a9 \u2014 purely resistive 2:1 mismatch (\u0393 = 0.333+j0). " + + "The marker is on the positive real axis.", + run: (dispatch) => { + dispatch({ type: "SET_FREQUENCY", frequency: 2.4e9 }); + dispatch({ type: "ADD_MARKER", gamma: { re: 0.3333, im: 0 } }); + }, + }, + { + description: + "Add a 0.152\u03bb transmission line. This rotates the point clockwise " + + "to land on the g=1 conductance circle (seen in Y or ZY view). " + + "Any point on g=1 can be matched with a single shunt element.", + run: (_dispatch, addChainStep) => { + addChainStep({ re: 0.3333, im: 0 }, "TL", 0.152, 2.4e9); + }, + }, + { + description: + "Add a 4.7 nH shunt inductor. Its negative susceptance cancels the " + + "remaining positive susceptance at the g=1 point. The arc drops to " + + "the chart centre: \u0393\u22480, perfect 50\u03a9 match.", + run: (_dispatch, addChainStep) => { + addChainStep({ re: -0.1109, im: -0.3143 }, "shuntL", 4.7e-9, 2.4e9); + }, + }, + ], + }, + { + id: "l-network", + title: "L-Network Matching", + summary: "Match a low-impedance PA load at 900 MHz.", + steps: [ + { + description: + "Place Z\u2097 = 10+j15 \u03a9 \u2014 typical handset PA output at 900 MHz " + + "(\u0393 \u2248 \u22120.57+j0.39, VSWR \u2248 4.5). The marker is deep in the left half.", + run: (dispatch) => { + dispatch({ type: "SET_FREQUENCY", frequency: 900e6 }); + dispatch({ type: "ADD_MARKER", gamma: { re: -0.569, im: 0.392 } }); + }, + }, + { + description: + "Add a 0.884 nH series inductor. This moves the point UP along the " + + "constant-R=0.2 circle (increasing reactance) until it reaches the g=1 " + + "conductance circle. Series components move along constant-R arcs.", + run: (_dispatch, addChainStep) => { + addChainStep({ re: -0.569, im: 0.392 }, "seriesL", 0.884e-9, 900e6); + }, + }, + { + description: + "Add a 7.07 pF shunt capacitor. Its positive susceptance cancels the " + + "inductive susceptance at the g=1 point. The arc swings into the " + + "chart centre: \u0393\u22480. Match complete with two components.", + run: (_dispatch, addChainStep) => { + addChainStep({ re: -0.5, im: 0.5 }, "shuntC", 7.07e-12, 900e6); + }, + }, + ], + }, + { + id: "antenna-2g4", + title: "2.4 GHz Antenna Match", + summary: "Match a PCB antenna that sits just outside VSWR=2.", + steps: [ + { + description: + "Place measured antenna impedance Z\u2097 = 25\u2212j25 \u03a9 (\u0393 = \u22120.2\u2212j0.4). " + + "VSWR \u2248 2.6 \u2014 outside the shaded VSWR=2 acceptance circle. " + + "The goal is to bring it inside.", + run: (dispatch) => { + dispatch({ type: "SET_FREQUENCY", frequency: 2.4e9 }); + dispatch({ type: "ADD_MARKER", gamma: { re: -0.2, im: -0.4 } }); + dispatch({ type: "SET_VSWR", vswr: 2.0 }); + dispatch({ type: "SET_VSWR_SHADED", shaded: true }); + }, + }, + { + description: + "Add a 3.32 nH series inductor. The antenna impedance has capacitive " + + "reactance; the inductor cancels it and moves the point up the " + + "constant-R=0.5 circle to the g=1 conductance circle.", + run: (_dispatch, addChainStep) => { + addChainStep({ re: -0.2, im: -0.4 }, "seriesL", 3.32e-9, 2.4e9); + }, + }, + { + description: + "Add a 1.33 pF shunt capacitor. The arc swings into the chart centre, " + + "well inside the VSWR=2 circle. The antenna is now matched to 50\u03a9 " + + "with just two components: a 3.32 nH series inductor and a 1.33 pF shunt capacitor.", + run: (_dispatch, addChainStep) => { + addChainStep({ re: -0.2, im: 0.4 }, "shuntC", 1.33e-12, 2.4e9); + }, + }, + ], + }, +]; diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartVswr.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartVswr.ts new file mode 100644 index 000000000..96f337e99 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartVswr.ts @@ -0,0 +1,286 @@ +import { ArcAnnotationBase } from "scichart/Charting/Visuals/Annotations/ArcAnnotationBase"; +import { WebGlRenderContext2D } from "scichart/Charting/Drawing/WebGlRenderContext2D"; +import { CoordinateCalculatorBase } from "scichart/Charting/Numerics/CoordinateCalculators/CoordinateCalculatorBase"; +import { ESurfaceType } from "scichart/types/SurfaceType"; +import { getWebGlPenFromCache } from "scichart/Charting/Drawing/Pen2DCache"; +import { SmithState, SmithAction } from "./useSmithChart"; +import { Rect } from "scichart/Core/Rect"; +import { getArcParams, getArcVertex, getVectorArcVertex } from "scichart/Charting/Visuals/Helpers/NativeObject"; +import { ChartModifierBase2D } from "scichart/Charting/ChartModifiers/ChartModifierBase2D"; +import { ModifierMouseArgs } from "scichart/Charting/ChartModifiers/ModifierMouseArgs"; +import { SciChartSurface } from "scichart/Charting/Visuals/SciChartSurface"; +import { CustomAnnotation } from "scichart/Charting/Visuals/Annotations/CustomAnnotation"; +import { EExecuteOn } from "scichart/types/ChartModifiers/ExecuteOn"; +import { EAnnotationType } from "scichart/Charting/Visuals/Annotations/IAnnotation"; +import { ECoordinateMode } from "scichart/Charting/Visuals/Annotations/AnnotationBase"; + +// ── Shared helper ───────────────────────────────────────────────────────────── +// Computes the centre and radius of the VSWR circle in screen pixel space. +function getVswrCirclePixels( + xCalc: CoordinateCalculatorBase, + yCalc: CoordinateCalculatorBase, + vpH: number, + dataRadius: number +): { cx: number; cy: number; radius_px: number; aspectRatio: number } { + const cx = xCalc.getCoordinate(0); + const cy = vpH - yCalc.getCoordinate(0); + const radius_px = Math.abs(xCalc.getCoordWidth(dataRadius)); + const aspectRatio = Math.abs(xCalc.getCoordWidth(1)) / Math.abs(yCalc.getCoordWidth(1)); + return { cx, cy, radius_px, aspectRatio }; +} + +// ── SmithVswrFillAnnotation ─────────────────────────────────────────────────── +// Fills the VSWR disk using drawMode=0 (sector fill) centred at the true +// circumcircle centre (0, 0) in data space. + +class SmithVswrFillAnnotation extends ArcAnnotationBase { + public readonly type = EAnnotationType.RenderContextArcAnnotation; + public readonly surfaceTypes: ESurfaceType[] = [ESurfaceType.SciChartSurfaceType]; + private _radius: number = 0.333; + + constructor() { + super({ fill: "#FFAA0033", strokeThickness: 0 }); + } + + set radius(r: number) { + if (r !== this._radius) { + this._radius = r; + this.notifyPropertyChanged("radius"); + } + } + get radius(): number { + return this._radius; + } + + public override drawWithContext( + renderContext: WebGlRenderContext2D, + xCalc: CoordinateCalculatorBase, + yCalc: CoordinateCalculatorBase, + seriesViewRect: Rect, + surfaceViewRect: Rect, + chartViewRect: Rect + ): void { + const brush = this.getBrush(); + if (!brush) return; + + // drawArcs requires a real WASM pen — undefined causes a toWireType crash. + // The zero-thickness strokePenCache pen is invisible but satisfies the API. + const strokePen = getWebGlPenFromCache(this.strokePenCache); + if (!strokePen) return; + + const wasmContext = renderContext.webAssemblyContext; + const { cx, cy, radius_px, aspectRatio } = getVswrCirclePixels( + xCalc, + yCalc, + this.getViewportHeight(), + this._radius + ); + + const vecArcs = getVectorArcVertex(wasmContext); + const arc = getArcVertex(wasmContext); + // drawMode=0: sector fill; sweep [0, 2π] fills the full disk. + const arcParams = getArcParams(wasmContext, cx, cy, 0, 2 * Math.PI, radius_px, 0, 0, aspectRatio, 0); + if (!arcParams || !vecArcs || !arc) return; + arc.MakeCircularArc(arcParams); + vecArcs.push_back(arc); + + const clipRect = this.getClippingRect(this.clipping, seriesViewRect, surfaceViewRect, chartViewRect); + renderContext.drawArcs( + vecArcs, + cx, + cy, + 0, + clipRect, + strokePen.scrtPen, + brush, + seriesViewRect.left, + seriesViewRect.top + ); + } +} + +// ── SmithVswrStrokeAnnotation ───────────────────────────────────────────────── +// Draws the VSWR circle outline using drawMode=1 (gridline/antialiased stroke) +// centred at the true circumcircle centre (0, 0) in data space. + +class SmithVswrStrokeAnnotation extends ArcAnnotationBase { + public readonly type = EAnnotationType.RenderContextArcAnnotation; + public readonly surfaceTypes: ESurfaceType[] = [ESurfaceType.SciChartSurfaceType]; + private _radius: number = 0.333; + + constructor() { + super({ + isLineMode: true, + stroke: "#FFAA00", + strokeThickness: 1.5, + strokeDashArray: [8, 4], + }); + } + + set radius(r: number) { + if (r !== this._radius) { + this._radius = r; + this.notifyPropertyChanged("radius"); + } + } + get radius(): number { + return this._radius; + } + + public override drawWithContext( + renderContext: WebGlRenderContext2D, + xCalc: CoordinateCalculatorBase, + yCalc: CoordinateCalculatorBase, + seriesViewRect: Rect, + surfaceViewRect: Rect, + chartViewRect: Rect + ): void { + const strokePen = getWebGlPenFromCache(this.strokePenCache); + if (!strokePen) return; + + const wasmContext = renderContext.webAssemblyContext; + const { cx, cy, radius_px, aspectRatio } = getVswrCirclePixels( + xCalc, + yCalc, + this.getViewportHeight(), + this._radius + ); + + const vecArcs = getVectorArcVertex(wasmContext); + const arc = getArcVertex(wasmContext); + // drawMode=1: gridline/antialiased stroke; sweep [0, 2π] draws the full circle. + const arcParams = getArcParams( + wasmContext, + cx, + cy, + 0, + 2 * Math.PI, + radius_px, + 0, + 1, + aspectRatio, + this.strokeThickness + ); + if (!arcParams || !vecArcs || !arc) return; + arc.MakeCircularArc(arcParams); + vecArcs.push_back(arc); + + const clipRect = this.getClippingRect(this.clipping, seriesViewRect, surfaceViewRect, chartViewRect); + renderContext.drawArcs( + vecArcs, + cx, + cy, + 0, + clipRect, + strokePen.scrtPen, + undefined, + seriesViewRect.left, + seriesViewRect.top + ); + } +} + +// ── SmithVswrModifier ──────────────────────────────────────────────────────── +// Runs before SmithClickModifier — must be added to surface first. + +class SmithVswrModifier extends ChartModifierBase2D { + readonly type = "SmithVswrModifier"; + private dragging = false; + private handleRe = 0.333; // known handle x in data coords + private dispatch: (a: SmithAction) => void = () => {}; + + setHandleRe(re: number) { + this.handleRe = re; + } + + setDispatch(d: (a: SmithAction) => void) { + this.dispatch = d; + } + + override modifierMouseDown(args: ModifierMouseArgs): void { + super.modifierMouseDown(args); + this.dragging = false; + if (!this.checkExecuteConditions(args).isPrimary) return; + const xCalc = this.parentSurface.xAxes.get(0).getCurrentCoordinateCalculator(); + const yCalc = this.parentSurface.yAxes.get(0).getCurrentCoordinateCalculator(); + const svr = this.parentSurface.seriesViewRect; + const hx = xCalc.getCoordinate(this.handleRe); + const hy = yCalc.getCoordinate(0); + const dx = args.mousePoint.x - svr.left - hx; + const dy = args.mousePoint.y - svr.top - hy; + if (dx * dx + dy * dy <= 12 * 12) { + this.dragging = true; + args.handled = true; + } + } + + override modifierMouseMove(args: ModifierMouseArgs): void { + super.modifierMouseMove(args); + if (!this.dragging) return; + const xCalc = this.parentSurface.xAxes.get(0).getCurrentCoordinateCalculator(); + const svr = this.parentSurface.seriesViewRect; + const re = Math.min(Math.max(xCalc.getDataValue(args.mousePoint.x - svr.left), 0.001), 0.999); + const vswr = (1 + re) / (1 - re); + this.dispatch({ type: "SET_VSWR", vswr }); + args.handled = true; + } + + override modifierMouseUp(args: ModifierMouseArgs): void { + super.modifierMouseUp(args); + this.dragging = false; + } +} + +// ── SmithVswrAdapter ────────────────────────────────────────────────────────── + +export class SmithVswrAdapter { + private surface: SciChartSurface; + private vswrFill: SmithVswrFillAnnotation; + private vswrStroke: SmithVswrStrokeAnnotation; + private handle: CustomAnnotation; + private modifier: SmithVswrModifier; + + constructor(surface: SciChartSurface) { + this.surface = surface; + + // Fill disk added first so it renders beneath the dashed stroke + this.vswrFill = new SmithVswrFillAnnotation(); + this.vswrFill.isHidden = true; + surface.annotations.add(this.vswrFill); + + this.vswrStroke = new SmithVswrStrokeAnnotation(); + surface.annotations.add(this.vswrStroke); + + // Draggable handle at (r, 0) on real axis — visual dot + this.handle = new CustomAnnotation({ + x1: 0.333, + y1: 0, + xCoordinateMode: ECoordinateMode.DataValue, + yCoordinateMode: ECoordinateMode.DataValue, + svgString: ``, + }); + surface.annotations.add(this.handle); + + // Modifier handles drag — must be added before SmithClickModifier + this.modifier = new SmithVswrModifier({ executeCondition: { button: EExecuteOn.MouseLeftButton } }); + surface.chartModifiers.add(this.modifier); + } + + setDispatch(d: (a: SmithAction) => void) { + this.modifier.setDispatch(d); + } + + update(state: SmithState): void { + const r = (state.vswr - 1) / (state.vswr + 1); + + this.vswrStroke.radius = r; + this.handle.x1 = r; + this.handle.y1 = 0; + this.modifier.setHandleRe(r); + + this.vswrFill.radius = r; + this.vswrFill.isHidden = !state.vswrShaded; + this.vswrStroke.isHidden = !state.vswrOutline; + this.handle.isHidden = !state.vswrOutline; + } +} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/useSmithChart.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/useSmithChart.ts new file mode 100644 index 000000000..bcc884ea7 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/useSmithChart.ts @@ -0,0 +1,170 @@ +import { useReducer } from "react"; + +export type GammaPoint = { re: number; im: number }; + +export type DragMode = "free" | "gamma" | "R" | "X" | "G" | "B"; + +export type Marker = { + id: string; + label: string; + gamma: GammaPoint; + isChainStart: boolean; + dragMode: DragMode; +}; + +export type ComponentType = "seriesL" | "seriesC" | "seriesR" | "shuntL" | "shuntC" | "shuntR" | "TL"; + +export type ChainStep = { + id: string; + type: ComponentType; + value: number; // SI units (H, F, Ω) or wavelengths for TL + frequency: number; // Hz at time of adding step + fromGamma: GammaPoint; + toGamma: GammaPoint; + arcPoints: GammaPoint[]; +}; + +export type GridMode = "Z" | "Y" | "ZY"; + +export type SmithState = { + markers: Marker[]; + chain: ChainStep[]; + chainStartGamma: GammaPoint | null; // when chain does not start from a marker + activeMarkerId: string | null; + frequency: number; // Hz, global for reactive components + vswr: number; + vswrShaded: boolean; + vswrOutline: boolean; + gridMode: GridMode; + zOpacity: number; // 0–1 + yOpacity: number; // 0–1 + activeScenarioId: string | null; + scenarioSteps: string[]; + _nextMarkerNum: number; +}; + +export type SmithAction = + | { type: "ADD_MARKER"; gamma: GammaPoint } + | { type: "MOVE_MARKER"; id: string; gamma: GammaPoint } + | { type: "REMOVE_MARKER"; id: string } + | { type: "SET_ACTIVE_MARKER"; id: string | null } + | { type: "SET_CHAIN_START"; gamma: GammaPoint } + | { type: "PROMOTE_TO_CHAIN_START"; id: string } + | { type: "ADD_CHAIN_STEP"; step: ChainStep } + | { type: "UNDO_CHAIN_STEP" } + | { type: "SET_VSWR"; vswr: number } + | { type: "SET_VSWR_SHADED"; shaded: boolean } + | { type: "SET_VSWR_OUTLINE"; outline: boolean } + | { type: "SET_GRID_MODE"; mode: GridMode } + | { type: "SET_Z_OPACITY"; opacity: number } + | { type: "SET_Y_OPACITY"; opacity: number } + | { type: "SET_FREQUENCY"; frequency: number } + | { type: "CLEAR" } + | { type: "LOAD_SCENARIO"; id: string; steps: string[] } + | { type: "SET_DRAG_MODE"; id: string; mode: DragMode }; + +export function initialSmithState(): SmithState { + return { + markers: [], + chain: [], + chainStartGamma: null, + activeMarkerId: null, + frequency: 1e9, + vswr: 2.0, + vswrShaded: false, + vswrOutline: true, + gridMode: "Z", + zOpacity: 1.0, + yOpacity: 0.7, + activeScenarioId: null, + scenarioSteps: [], + _nextMarkerNum: 1, + }; +} + +export function smithReducer(state: SmithState, action: SmithAction): SmithState { + switch (action.type) { + case "ADD_MARKER": { + const num = state._nextMarkerNum; + const marker: Marker = { + id: `m-${num}`, + label: `M${num}`, + gamma: action.gamma, + isChainStart: false, + dragMode: "free", + }; + return { + ...state, + markers: [...state.markers, marker], + activeMarkerId: marker.id, + _nextMarkerNum: num + 1, + }; + } + case "MOVE_MARKER": + return { + ...state, + markers: state.markers.map((m) => (m.id === action.id ? { ...m, gamma: action.gamma } : m)), + activeMarkerId: action.id, + }; + case "REMOVE_MARKER": { + const newMarkers = state.markers.filter((m) => m.id !== action.id); + const newActive = + state.activeMarkerId === action.id + ? newMarkers[newMarkers.length - 1]?.id ?? null + : state.activeMarkerId; + return { ...state, markers: newMarkers, activeMarkerId: newActive }; + } + case "SET_ACTIVE_MARKER": + return { ...state, activeMarkerId: action.id }; + case "PROMOTE_TO_CHAIN_START": + return { + ...state, + markers: state.markers.map((m) => ({ + ...m, + isChainStart: m.id === action.id, + })), + chainStartGamma: null, + chain: [], + }; + case "SET_CHAIN_START": + return { + ...state, + markers: state.markers.map((m) => ({ ...m, isChainStart: false })), + chainStartGamma: action.gamma, + chain: [], + }; + case "ADD_CHAIN_STEP": + return { ...state, chain: [...state.chain, action.step] }; + case "UNDO_CHAIN_STEP": + return { ...state, chain: state.chain.slice(0, -1) }; + case "SET_VSWR": + return { ...state, vswr: Math.max(1.001, action.vswr) }; + case "SET_VSWR_SHADED": + return { ...state, vswrShaded: action.shaded }; + case "SET_VSWR_OUTLINE": + return { ...state, vswrOutline: action.outline }; + case "SET_GRID_MODE": + return { ...state, gridMode: action.mode }; + case "SET_Z_OPACITY": + return { ...state, zOpacity: Math.min(1, Math.max(0, action.opacity)) }; + case "SET_Y_OPACITY": + return { ...state, yOpacity: Math.min(1, Math.max(0, action.opacity)) }; + case "SET_FREQUENCY": + return { ...state, frequency: action.frequency }; + case "CLEAR": + return initialSmithState(); + case "LOAD_SCENARIO": + return { ...state, activeScenarioId: action.id, scenarioSteps: action.steps }; + case "SET_DRAG_MODE": + return { + ...state, + markers: state.markers.map((m) => (m.id === action.id ? { ...m, dragMode: action.mode } : m)), + }; + default: + return state; + } +} + +export function useSmithChart() { + return useReducer(smithReducer, undefined, initialSmithState); +} diff --git a/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/vanilla.ts b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/vanilla.ts new file mode 100644 index 000000000..4f87ed08b --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/vanilla.ts @@ -0,0 +1,19 @@ +import { drawExample } from "./drawExample"; + +/** + * Creates charts on the provided root elements + * @returns cleanup function + */ +const create = async () => { + const { sciChartSurface } = await drawExample("chart"); + + const destructor = () => { + sciChartSurface.delete(); + }; + + return destructor; +}; + +create(); + +// call the `destructor` returned by the `create` promise to dispose the charts when necessary diff --git a/Examples/src/components/Examples/FeaturedApps/ShowCases/DynamicLayout/index.tsx b/Examples/src/components/Examples/FeaturedApps/ShowCases/DynamicLayout/index.tsx index 1c9dd9d0f..9df13ebb1 100644 --- a/Examples/src/components/Examples/FeaturedApps/ShowCases/DynamicLayout/index.tsx +++ b/Examples/src/components/Examples/FeaturedApps/ShowCases/DynamicLayout/index.tsx @@ -35,12 +35,8 @@ const ChartToolbar = () => { color="primary" aria-label="small outlined button group" > - - Single Chart - - - Chart Per Series - + Single Chart + Chart Per Series ); }; diff --git a/Examples/src/components/Examples/FeaturedApps/ShowCases/HeatmapInteractions/index.tsx b/Examples/src/components/Examples/FeaturedApps/ShowCases/HeatmapInteractions/index.tsx index e3f473dd8..9ed074fa4 100644 --- a/Examples/src/components/Examples/FeaturedApps/ShowCases/HeatmapInteractions/index.tsx +++ b/Examples/src/components/Examples/FeaturedApps/ShowCases/HeatmapInteractions/index.tsx @@ -17,7 +17,6 @@ export default function HeatmapInteractions() { onClick={() => { controlsRef.current.stopUpdate(); }} - > Start @@ -25,7 +24,6 @@ export default function HeatmapInteractions() { onClick={() => { controlsRef.current.stopUpdate(); }} - > Stop @@ -33,7 +31,6 @@ export default function HeatmapInteractions() { onClick={() => { controlsRef.current.twoPoint(); }} - > Load basic example @@ -41,7 +38,6 @@ export default function HeatmapInteractions() { onClick={() => { controlsRef.current.interference(); }} - > Load double slit example @@ -50,7 +46,6 @@ export default function HeatmapInteractions() { onClick={() => { controlsRef.current.showHelp(); }} - > Show Help diff --git a/Examples/src/components/Examples/FeaturedApps/ShowCases/OilAndGasDashboard/index.tsx b/Examples/src/components/Examples/FeaturedApps/ShowCases/OilAndGasDashboard/index.tsx index 3fb660788..cd8f47d5c 100644 --- a/Examples/src/components/Examples/FeaturedApps/ShowCases/OilAndGasDashboard/index.tsx +++ b/Examples/src/components/Examples/FeaturedApps/ShowCases/OilAndGasDashboard/index.tsx @@ -63,10 +63,7 @@ export default function OilAndGasDashboardShowcase() { {isXs ? null : (
-