From ad8abd2f6080108e506cccfdaf88a24f376847ee Mon Sep 17 00:00:00 2001 From: antichaosdb Date: Wed, 22 Apr 2026 17:12:34 +0100 Subject: [PATCH 1/3] feat(SmithChart): add interactive Smith Chart example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full Smith chart implementation using SciChart.js with custom WebGL axes: - SmithChartResistanceAxis / SmithChartReactanceAxis draw constant-R circles and constant-X arcs via WASM arc primitives; outer rim ring with tick marks - SmithChartAdmittanceResistanceAxis / SmithChartAdmittanceReactanceAxis mirror the above for the Y (admittance) overlay; G/B labels at unit-circle intersections - SmithGridCalculator computes major/minor grid values in s-space for perceptually uniform density, clipping minor arcs near the (1,0) singularity - SmithMarkersAdapter: click-to-place numbered markers with constrained drag modes (free, constant |Γ|, R, X, G, B) implemented as ChartModifierBase subclasses - SmithVswrAdapter: draggable VSWR circle with optional fill shading - SmithChainAdapter: step-by-step matching-network builder for series/shunt L/C/R and transmission-line components, computing new Γ analytically at each step - Z/Y/ZY grid toggle, independent Z and Y opacity sliders, grid density controls - FloatingPanel component (draggable on desktop, SwipeableDrawer on mobile) - Responsive layout: column flex on sm breakpoint, chart fills full screen width - Marker panel and readout table fully themed via CSS variables (navy/dark/light) - exampleInfo.tsx with full markdownContent for React, JavaScript and Angular Co-Authored-By: Claude Sonnet 4.6 --- Examples/package-lock.json | 15 + Examples/package.json | 1 + Examples/scripts/enforce-trailing-slashes.js | 2 +- Examples/scripts/linkcheck.js | 52 +- .../AppFooter/AppFooter.module.scss | 4 +- .../src/components/AppRouter/examplePaths.ts | 5 +- Examples/src/components/AppRouter/examples.ts | 1 + .../src/components/AppTopBar/AppBarTop.tsx | 23 +- .../components/CodePreview/CodePreview.tsx | 99 +- .../components/CodePreview/index.module.scss | 42 +- .../StartupAnimation/drawExample.ts | 2 +- .../Animations/StyleAnimation/index.tsx | 8 +- .../ImageLabels/drawExample.ts | 2 +- .../MultiLineLabels/index.tsx | 12 +- .../ForceDirectedGraph/airportData.ts | 1495 +++- .../ForceDirectedGraph/drawExample.ts | 104 +- .../ForceDirectedGraph/exampleInfo.tsx | 9 +- .../ForceDirectedGraph/nodeModifiers.ts | 52 +- .../SmoothStackedMountainChart/index.tsx | 8 +- .../Filters/PercentageChange/index.tsx | 8 +- .../DiscontinuousDateAxisComparison/index.tsx | 8 +- .../DrawBehindAxes/index.tsx | 8 +- .../LogarithmicAxis/index.tsx | 12 +- .../ModifyAxisBehavior/StaticAxis/index.tsx | 8 +- .../MultiChart/SyncMultiChart/index.tsx | 7 +- .../PolarColumnCategoryChart/drawExample.ts | 47 +- .../PolarCharts/PolarColumnChart/README.md | 97 +- .../PolarColumnChart/drawExample.ts | 22 +- .../PolarGaugeChart/drawExample.ts | 10 +- .../PolarCharts/PolarLabelMode/drawExample.ts | 20 +- .../PolarCharts/PolarLabelMode/index.tsx | 35 +- .../PolarLineMultiCycle/drawExample.ts | 2 +- .../PolarCharts/PolarMountainChart/README.md | 43 +- .../PolarMountainChart/drawExample.ts | 32 +- .../drawExample.ts | 47 +- .../PolarRadarChart/drawExample.ts | 47 +- .../PolarRangeColumnChart/drawExample.ts | 2 +- .../PolarScatterChart/drawExample.ts | 46 +- .../PolarStackedMountainChart/drawExample.ts | 37 +- .../drawExample.ts | 46 +- .../PolarUniformHeatmapChart/README.md | 101 +- .../PolarWindroseColumnChart/drawExample.ts | 24 +- .../TooltipsAndHittest/HitTestAPI/index.tsx | 12 +- .../OverviewForSubCharts/drawExample.ts | 4 +- .../PolarModifiers/drawExample.ts | 17 +- .../PolarModifiers/index.tsx | 118 +- .../v4Charts/AnimatedColumns/README.md | 13 +- .../v4Charts/BoxPlotChart/drawExample.ts | 9 +- .../v4Charts/LinearGauges/exampleInfo.tsx | 6 +- .../v4Charts/MapExample/drawExample.ts | 8 +- .../Load1MillionPoints/index.tsx | 14 +- .../PerformanceDemos/Load500By500/index.tsx | 6 +- .../MillionPointsSvgOrNativeCursor/index.tsx | 8 +- .../RealtimeGhostedTraces/drawExample.ts | 6 +- .../ScientificCharts/SmithChart/README.md | 68 + .../SmithChart/SMITH_CHART_RESEARCH.md | 185 + .../ScientificCharts/SmithChart/angular.ts | 15 + .../SmithChart/drawExample.ts | 135 + .../SmithChart/exampleInfo.tsx | 167 + .../ScientificCharts/SmithChart/index.tsx | 877 ++ .../SmithChart/smith-chart.jpg | Bin 0 -> 116180 bytes .../SmithChart/smithChartAdmittance.ts | 373 + .../SmithChart/smithChartAxes.ts | 840 ++ .../SmithChart/smithChartChain.ts | 150 + .../SmithChart/smithChartGridCalculator.ts | 444 + .../SmithChart/smithChartMarkers.ts | 566 ++ .../SmithChart/smithChartRim.ts | 96 + .../SmithChart/smithChartScenarios.ts | 160 + .../SmithChart/smithChartVswr.ts | 286 + .../SmithChart/useSmithChart.ts | 170 + .../ScientificCharts/SmithChart/vanilla.ts | 19 + .../ShowCases/DynamicLayout/index.tsx | 8 +- .../ShowCases/HeatmapInteractions/index.tsx | 5 - .../ShowCases/OilAndGasDashboard/index.tsx | 9 +- .../Examples/FloatingPanel/index.tsx | 111 + .../Examples/styles/Examples.module.scss | 11 +- Examples/src/components/Examples/theme.ts | 4 +- Examples/src/components/index.scss | 16 +- .../src/helpers/shared/Helpers/Context.tsx | 8 +- .../Theming2D/src/CustomLightTheme.ts | 132 +- .../Theming2D/src/colorPalette.ts | 8 +- .../Theming2D/src/drawExample.ts | 722 +- .../CustomerExamples/Theming2D/src/index.html | 4 +- .../CustomerExamples/Theming2D/tsconfig.json | 59 +- .../Theming2D/webpack.config.js | 22 +- .../plans/2026-03-07-app-refactor-design.md | 3 + .../docs/plans/2026-03-07-app-refactor.md | 123 +- .../2026-03-09-mobile-optimisation-design.md | 26 +- .../plans/2026-03-09-mobile-optimisation.md | 69 +- .../docs/plans/2026-03-09-pwa-design.md | 18 +- .../plans/2026-03-09-pwa-implementation.md | 120 +- .../WebsiteDemos/ScichartSDR/eslint.config.js | 18 +- Sandbox/WebsiteDemos/ScichartSDR/index.html | 7 +- .../ScichartSDR/public/scichart/scichart2d.js | 7753 ++++++++++++++++- .../ScichartSDR/pwa-assets.config.ts | 6 +- Sandbox/WebsiteDemos/ScichartSDR/src/App.css | 28 +- Sandbox/WebsiteDemos/ScichartSDR/src/App.tsx | 362 +- .../src/components/LiveSignalMeter.tsx | 6 +- .../src/components/OfflineNotice.tsx | 24 +- .../src/components/ReceiverControls.tsx | 676 +- .../src/components/SignalMeter.tsx | 18 +- .../src/components/SpectrumChart.tsx | 74 +- .../src/components/WaterfallChart.tsx | 48 +- .../src/components/scichartCreateLock.ts | 4 +- .../src/components/signalPalette.ts | 12 +- .../src/features/receiver/constants.ts | 72 +- .../features/receiver/hooks/useFrequency.ts | 92 +- .../features/receiver/hooks/usePinchZoom.ts | 2 +- .../src/features/receiver/hooks/useRadio.ts | 33 +- .../receiver/hooks/useRadioLiveData.ts | 2 +- .../receiver/hooks/useReceiverSettings.ts | 36 +- .../receiver/hooks/useScreenWakeLock.ts | 19 +- .../src/features/receiver/modeHelpers.ts | 5 +- .../features/receiver/performanceProfile.ts | 20 +- .../src/features/receiver/presetsStorage.ts | 13 +- .../src/features/receiver/radioHelpers.ts | 34 +- .../src/features/receiver/settingsStorage.ts | 20 +- .../src/features/receiver/types.ts | 13 +- Sandbox/WebsiteDemos/ScichartSDR/src/main.tsx | 14 +- .../WebsiteDemos/ScichartSDR/vite.config.ts | 65 +- 120 files changed, 16520 insertions(+), 1851 deletions(-) create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/README.md create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/SMITH_CHART_RESEARCH.md create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/angular.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/drawExample.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/exampleInfo.tsx create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/index.tsx create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smith-chart.jpg create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartAdmittance.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartAxes.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartChain.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartGridCalculator.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartMarkers.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartRim.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartScenarios.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartVswr.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/useSmithChart.ts create mode 100644 Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/vanilla.ts create mode 100644 Examples/src/components/Examples/FloatingPanel/index.tsx 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({ clipOffset: 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 0000000000000000000000000000000000000000..969e60a493bcc878d51380101ec9079217f644e4 GIT binary patch literal 116180 zcmbTdbzB@lv@STfLvVM80KpwXa0pIt3C;k628R$_f@?yM;O_435Zng$;0!wWo7{W% zzV~+j*tgr$-M^{s>eF?qyHB0-ov&V(UN-@6l;jlU0B~?{fX}cW;B^Hc4M2d0|F^;x zMA(XiiiCuSh=h)Uf{coZj){qZj)8%NjrRr%8y6b`;|JiN zw6eCb1=_iNarf}_^7aW12@MO6h>S{1N=`{l`|&e9H!r`Su&B7Cw6?Ckp|PpCrM0)O ze_(KEcw}^Dc5Z%QacLR6wY{^uw}0^W@Cb5wb$xSt2fcszrxzRm;Xl>-Z_WO1df~$K zf=5I|Kt%bc7aY7NY(v0BM55(J#*@-OF>}GE;|WA1_>hoO(}PCOt9e0a?mCT5#K5=3 z2>GYle`xl9rdZJbFU|g2vHz(T7=VcY2YYx3xBy826mMpl@9$GHWZwdZ=19#Mr|7DL zrDDxgx3hYl^AzN`-_R7K6D716pZhI8uv;uknE!h2a1ggvOr*hjKT+558Pw38?S`FN zFS$iGAMlsi`xOv~^TOA4kzm(#jo%cUI9{~ zlTWAtkb+l0$oEgLfZv%1FNl)R*?QksfT@?8&daB}t{0if-1>n3O>#i5Z0RdtO7-d$ z08jm7`~OmV7eZB7iLqhy@%I(Q46)oTn<{GQe_qm3_OCs!06OcL`3dti4u0Ri^dzyY zYXm-ke)J;E`OFwIMxheU9A#MoB7%C&DOy(B+(0CPdM!Y1YLq!TOTlkM*dG8(?!PUK z0FbP?9jy+pp&Z5i9!-{$WdOiP(46=0@HDJU@b8Q)>`;9&Aeqd71+HE*omJ0VQ*&AY zkuNM$@qK@OL2h8oKY_s9)Wmo+S=Rh~crs~K*j`gJodvEiSXsdq?%&PW7bI}K410A0 za3pA;RRd&JXVrd^w@7uh_3e*5qgNwz)ZC3VV1beylxWZ4OP>}{`NlGEvDLrhW}Jw) z0BgQni)TRigOZcF^NJ#ajS$0jq0-SXa5?+^(4(X+-qXjIeuW$2nXWBl*kg$s*XQv& zueon)0u4^`Mt>!(l4*l#ZJ82#IY~owqc~4Zu<(0KsDnf?#tt1enJm-p>XZq1T8mdg zd%x*zR{#BKP!Da{DZ_%}V|OC+3fquT05i^-&)XPUeL%Is4F|&^264x#Nag#fK;pvB zw>oE@^ecf))!>R~PXV9gX@%6|J2M>}L^)BGO&=Xje18?;+*a}~3NifOP_7|Coppk) zH0>!&=iwwFk2eaCa9hk6GN+Y{FmQ_|Hv6Q#lL2HX;zqpm%lZ-q+%!#+L}AMN-B0Hz!0e z>_-w(7i7PDHw;6a`)Mg;OTKRrBpMyWHn7_2Q9(ddYo{|U=1PCeTNe6gGXLlApRca( z#)j6-$uo9cvM*wfx~?(*ePW)k0CZERyu0ZGvPr!iW|=)$7b0TdwF{w>4Qb!rrFRnSeJdD~%G$px{$%jBo`**h0J-Dv7 zvp44Hl~Y*QGTAaq)hCTWjhd*(lV@Ar{6zoA2W{O~!TnN}iP5iEx>1>&WZPOVsL|sw zs&AH(yrrnFV3r~z#MiR3>Q{al#lC9u{F7f2`XT(*m$PF>dRt~%B`d+3aH8B6WMG4R z^p29$d7{hio7=Hoy4uO&_L|)qLu1QS`&fxp_{XLs-9U!+`Fyk`@qVfw*O28y&< z73At=EC#AN_9P}tO>{g1@v+-lvU)Jt?uIDRf6EbwS0!h_9dKKxhca5^_&HuI2A^zb z%%3zgpHyo8k&7Br4|F7W7o^!UBbcw@Ahqz6=FmtL{y6hw9x`@|s`Y1TRY#UCDH^5mYM z(q1!mhE}3`OTr>i!7LhK>HvWMX@cBjd(S3Y!(^!R>!fZgPT(2C{Le2{#8TTou9?H` z)SJS3Xzit)8F*^Yc`xH1Z&T<2iR=;ww;)K+k%*B1PQJYWSv3CYK=~m!gUgJ<+2R!I z?QUqad_tRtNVE-InwW1myI5`1jZlD%8m^IvG09CFro?x9bQQq}Vxw<|kZ|*e3L0VT z@p5>=wH~1%zM8KSS=?b)+Hs#i^nD8kehIzc!kSWPq~uG5EK6m~m8el;iO*Uz*op-I zo@nP#%Ct!dByDrm6sMEj&`VII1&txq!?nG&;Tl(-Vy3t9u~3v=8&efcXi!+(-o7uO zHmjr3D@ma`shCoaC?gHW+p7}r}Qv> zQvAg84$UF;5UsypZ5RP~oU}B%ALpW5+pZSw6k*?@% z!Box;Z8a(>!M|^`s^)+|H^-}JlPmg(w8SfHOmEhrE=j8PyLzWbvozTNJKIo0U>`3p z2wiyCY-urTo++yrSkW8BQb>~<#R7LzCXi`iMH_Z>Yt^jEpuo{rnsuCbJp9+IymcBv z{1i=={p_#&3dl)sus0bJyU*A`Jk9eLYb#n6i(!8WaFV>hm4u#hB@_fqo*ulwGrs~T z%a7)!uOrU4+ge&OD;z35eXNzI{;^xrTUz;^AzM=j<*%K@Rjujbx{cPI#zCgMlkB3} z9_cR7^y(4j;(m*K1q=cG{ukM%O&57bw`rbaG#&FqD3O@<)-u`eToutLzM=0_37={W zY!<~CJYwC90U-k*%7O9O4^qKCCFt7mZY}cxMmnV#*~>4TR|(sH2kMVSD-3G8R5O~6 z>KmJC=SDC2s_viv4f8UufSr18GTo<_^?*&f00?+)=80@E4Y$wJm_4iQUBo!B zxu)k}Hg2&%bFN)jAjRtKY%`v`+pdE~%4c0?cZW;VL2;=GW;DjHrpM=OLg|{>bul&p zvg;dTd(Xyi?*9pgxe)(^yBYu!n`OE(Nn;UI90*b7BGC{o_%mf~+?P(j96X(0z+7kL z2&x5d+IUc+088IgaPY?poHwt4;Q*~yKvhY=HLo^{-9&DQ;vd|kqj=b`Gi6Jlq5M85 ziq#g5)pmdbz{F|`bD|4!Q-7ez8qQpg#aO?vi)!N;DJjS@lXf$gJm{nbZZ+Q+vX4Ek zY!v|)$`jhx^%H(>h%_2yeBq{p*uEgHd@M8CYvypFyPSx^qoivTsN**pD%*PV< zx*{f^n>7z?SAkuemL~EslJJB|@qOqor{Vsm4M9qj3L=aQ*eI;=88*POAUO5Z8A?^M z8`T3JOAs}ccp_P)Ykv43M4garcx2j?tWG%jm-aY$_3BmkL?N*#4w6(}y~ui>Wfo5s zm@v-Qh5Y0VdQTJdP6{OmRj8-p6@as9FToCx$hNl;Zzq;ArqK+fl|>)#vkQ9nUc9Ap zlvmN!PzTmm2PMQ`9Yepz=oO~r9o@Yj@n$!AeB<%~@ny=#SVtGwWc~ zpD}zBiFQ9IaBi4`8g|b@%lcZ^o=Z1Wye;@M-WVDzd)JI#4Xd+{HGA98fXEG9gmy@C zbsB$HO$c1H3tVkSTwwOw()^ENoM8xk-wD7mFxOyOb2api!wlZdVYX&W&NF9Dbnx9(b zW#Pak;z3qRcZ+D=by`W7R!b-kE8Ali(yHsKu7QwEXlJDEyHKWWGveMBlu%(Kbr_SI);1hakOohtax* z1ux5JdA1%Ra@o0F-{ohoTbN%anr6gQ8~?_){lsGpthZoJ9q-Y{bkK#1q<8lwtm>|c zAkJcIzgYc>2a9qV9d_`I48=qdHL~02Ma@14TK~Z@2exu`Pf|mJjpo1jQlr@=9vNt0 z_#<>%2#kL{YHe;+bWZ;UG{ZwAERRt*=zsuqRm8*O?$6e zR~(IyI!e+?k^K-DtBWW|Crlcq{Si#F7W4Z|=rOOr^@S9Q z2EiE=s9pX0@@AfkCJM;0)-MY9iA}}zi@F_g$7cPH*LL<9_!7sTw`(c@7=tHzj=bTO z)zMnUJ_^|E+jwpN-UVIR=l2T_M4+&AMwO*oFK?h{C==-akeF~DK~q9}t36+itzPHtv9aKflQL!sHOl78fSQWQB0ELel; z+^M7)wiqhwSK&J-)9#i>^HztB5z>3W3Gs{7hkIvwe(M7IGjGL&mF0Qa^cn2O{XXLE zooQ%_By^{BBbPkyoW3>X0?IF0Tylq^2=C`Ws81P-psxV6dAWy3O`B`Wh)d-pavKQCG&wTVK2K{G=K$tZs^y8_Iq)pKQxZve(Drl2ElN zO8gwMyRIVe16+g$`LPz;gS%oIfFRn%vvx@bzAo$m;dDI$qJ*aPp&CU7Oh+No^C*y( z>a%GvGgG-0H2n5k9Ry{_pLv@i3#Z|cFvwt#5ADqFZ>Cv@oPj^ZVndw+5kMha2ZKU2-=6e;A=)DW9+=G^L}IAt)>A{H3DEd$J$_$19Qxm zu?{H%H;QU8PORSEvNk+!l+hto9N+Yz0e=xEBwG{WnJ?^R9HI&fd-Gd~cN&I~Mj~>Z zhJ<0<@fOU^2L24#F%labOqQCxyeDXgGmhjn zm7v%&wUyY*n<~5=`GxCak9nIpkk)$$-g0ezKCd*ly*^@|hk_Mu^$2Md$wkvZWV_@x z4!PZ;fUBJ+74ysumWWR3Zg@;(?8ou}MOSP^3dczeg|NvD2hzdciSN ztBu*mJ_P1QR#`2@CGwQ{$xl$T$4~1|{1=^}zkBkH#4}?Gb8OuCQwwhGBSXih$E)z~ zRrbU{Z;mMS@KefesT)rB()vl<$6|OtL9oA5{kp}^_X;*7{&RisT6%@FvTl=7l8eLn=ik4*IfS3VEqQOZRpEXie2?v~3fPSj2Da6#+YO(zgC1p%vP7VU zpe_4HB#}C!NPaA@am=PEh9pHq%wJx(9sMDcLt@U{lL@*Oo9E#?(8nQHTQbHcS0IG1 zPPu$d&MXK+;nL(t-BklVdh;p~MH9&&ZmXiQU{j6QD@|chVuL!|9hGA*@D$U;y?lIx zax`&>)E$sR3KuwO*Dywji+!%+oU9>htDqrE0LNhq{r)0{rS{SZL!PH5Y;>;a9yMaD zL8x|!(f+?>1q>-%HNG)#Q~(Lb%Uu`J>XPlm8w?9N+E!{pb+0Rouj@qa->n(`X=7~4 z5EugN`p=-!LmEU$>E?!XFuAC`sJDqQrn8d}3kkOVkE`|r=Gg*$39dP!xjqcW601luJy*(9zNOz8 z*XK7+Y^K#F0aGvsGrlqa+WNm0+Eg4~0g{*RTzeaUA6_cCn!N36H>#C)?C(RJ z1D-=@`4?rg=D02l8#~)5_C^{Yj=}#2(K6Ceo zeU&R$-0Dzb$7yWH`wq$(v$SOWt8^fAH(>g<4dbULFRQ(r6V4IQEru^+8AlD^d{_u2 zhh7-w;)dOQY?%$Q-livsw{n$w2oCPK6`LMoY{e*=D^Zr^ppArq|8TQVoI$58ECW_! z`ovjpAi>HI{qqd+Odoy57rOel?n{2Uxkn<_CH}xdqY*K|H%264&8po;qcZkAeU=m+ zl{&_BXOx^+vN;pUf2J8md%_^|syL%Af3SIkDLz%O_og@22TYw}p-bW$u#B`Z zD7&ohPewLSGV3kixw!8e0H+M}he+hGJR)+817D~@u8!DqA>mPPov!`Avby_%RM(Fb zR+Jlq1Qj{Dzt&pG%Z%*bxU z&Zzw@THkONvSMb^0o%h-)|UFN%6`_uQ%W^H1eb17iIiEd@NSXVm~HY3PB7~YE%LY; z1tRilV5{ZzACcS2p#kEDxKKZ>(!Z_p;T52P$m3q@4WKLwMLxvrreqd!zW${WjEqs6{`6D=j`$Myo);Rhy*v! zB7=lEBfs+X|pziv)AY)0?u>UCL@2zt9Tsl8ZM9LEBCP>G$Z`=2fEFu zj&eM`0yc3{^VV_$pgLs~bo%Ym1+t`|w)Po0e|Fz)D)QNO@$F-bTL@F8+a591N7*KU zzsp`v#~HE45~i3f;fm>>Vo#i}xF3hTH*wD9UIVlaBsrhzpiYR10-zW2% zn19)09NlXnexeaTIfGUyNow}yXKzI(NDM4~DRf0Z;yo3jr@1=)6qET!Rg9^5^3LiN zuwACy-^eRb9I*Yv8lEIfee4w=wz!_RqSe&{O&O8ge?sTDC^KN~Bz;?{C4|hLBFry$ zu(cRbA;nSf@klaC?AayLPXReCKhFa4VhBBJyE;AYlDayX^K|J?_+x(FT z@%PHzu=}_OvTc^nZ}HWehA|ZogeKk8UjYt%B?qTI5^*ID9pyp9Pbh=|Es07zO{K)= zL`N^->AGI&U19Ry(h-V`e!i@he}42iTie6k4=DSy3W-~2@{_z+l!we;FQmGd+UnwV z-@RRw_M=s|>U1h_!++v$ZpM62&9fFsd}pk$nwzQ{JAM4(`jM>4fTv zW{SMncCHXWxFbjJ>6q`sAL;3{QX;p`v5rpPmZrKH^)5ixl#BYf6*+%%^r*KtV&fDa zM-9*Mt!c@xVXX=h^|dzB1cdT{L|y@^wKa3bWaPdbuK=4LLie|n710#Y`0I6h{*7$M zEi+GAO_%)Qj2`@zd`sV|y@V`dWzvvC4m3f z^rHTW7(%|))gM_^>0Uwn@ulZtA&D=w`okSa42!LivL^b@{d zuscp&jMm4+->BtI*UG_e4y1JRO|#ha#k!tv?r!OTZZD9>!4+UDTwdC|p9V~Uvtu%(Ebvq-(qVFL)tX<8!N*`Eb=K&7;Sqd~@{GBQjJ7@UAx`7xs z7XZhiBRA+QRh3;{kENzr)7nu#TX|MqfqeY^$7h>Z+@lj@Vn~tHR)@*4TKWSe+pQsq zx+2G@VVgWz;(P1Q?~tW-A2d$3GyAd(snw2O0rhcxl)eLtA`d{3j;un?FFV*p0XfTd zi&*wf@bn*uyn8?FO;!Jt7Qm5;+UB!8iLGUJx7P}%On z=_x#3e8^0gDF+?c7js(bqbBqBqFQSp!#tU2k%D@VeU63?oaQG1K|TZsT&P{J^qSJWtrjy zeZpLI6w7i3XettxY*z)6orkt_cvn@HY(0C7sU@;B5Ki6LRxwlq(X{bGgnB;fx~7tn zOmHgr31R~sJ*iR8{RAYft0IZ080Ky$1!j(QXW=_jFg@|ZG}z%kdz<5N%1esu&|F1R zsuB7HT&Tr7QTuM%&u)wJ_DwAxdfbYv3OE~Ws4je`OLBwD;s^I?ePa|As9LW=>$>3V zD)h2Yg%JKpaUqH<=KBuv(Ph~~5Bno*V(ijJI!RhemK9oJTjl@`%Xa_V#C=~){)3F0 zEx_9ouGu%LQfBRp(>f)txI)sN3r&9ms8N_%*%6yi`nPsPjYsBAkf`xpyTj$n1spHY zu)0F`;2C@H1@+JbbMHcHb9>`l<$fz9D|1_~H-ybR$R<(N+%%Hp9ReUulYLP4!(7(b z>Gz^F{z7fyhq+n&S2y%sLRw|UDw zwiK75W2S^qyP@0AX}?be>qq#1?YA-fb%6Y$d=06zw>j*UbE)bdsvwS}2Z6?FLjj~q z`d+4*P!EgD8<6CnNLZ!gtcAgt6HgpVUN}SIyEX10;VPoL{LC4xRQgGLOj)wDC#Nn_so>KP}|8 z)rn>=!!T~nw2La0h105=I09g}W;%^-U*jzoxm*bi)2>mTe*v7!OSBTbRiwjq-AzFB z6!>MD=oNs=*TQx4%U5~DK3Sxn=ynl5-zAr)-eXhtO>cZ@2HPpcW)f4;qk!Fl1p1=b zC(!mJ>M@yWbgAQp$~l1*anB3fyqKF)hm4z74Vij z7q^&+{uF>8w;-2u$znH|+?fo_gJcClh`ikIAcZmcj8Q;E8^G+DqxrWUf>YsaAI`OM z3tYi*AqU;`Cs8QYc3fm9Wyj)|s*^{5rqRtF7x&_|iguUN=rQ_KM*%v(xUZuTyaGYf z27-G;#ZBb(sZC!eZMgERYMH1lINuZN)`c!FJ{S|7SW_BlbJsd0$5((j&CKB|6WGgU z^GSmA;=veveD~0LWYC#A8dX5^3W!rMr%C)!14N^BUK*73xjR{e7!JJx-birR`ou+v zeR3)GGVGg`$SEExF&UuH8QWDVqs5dGOBDLUBQEQeRdQiE;G-7OPP@%oj&S(G_H%O< z(GfHDXzN4mD*$`jwKFgM#O67sOQk(DV2A75FD4`a%sA(24kZ2joPrLGrl zrw1fz1eTRJg|pf`HvaHuw(l19u0aScjzq4(^~6N8-venqgNPg3RBn%)N2y88J-Oi9 zoBB<#7yOvFe@$ZBrTaC4*YaWBLF%~KM!;u*T>)2AdYT~AKXw?|n8!puzYb>)1*hy# z;p`PTGOg5Ig4Cm9^O}4r6_Um$10bfS_J!-v*@4+ZYMgp5sC>I@@qnktV%Ur5aY#%s zOG0i3LY0I;_S<-D@FFCxzYEjv{l&&07ZH}QpKvZTO>36#FW!dR`s!&5F}~8YqG;t! z3<~=2Pd1_5#qm2T%6SmKOMzOUiF7Fu>RZbu_l%mq0=R9=vxH6CRblCq!D8H7&rY9Y z8siKSkAy%JB1Rt;&Wg9St;k)CED#}!gc5()7GAo_aDE=SwdO7t7J0OONg6AP%rI1R zC(ExO+No|W`CKl?14l!#nblwr#qRbFmUe@;n}Lwm**6XAr9|08B71$({1A8oApB(@ zLHi0w2>!qFJO*5$8J917{?rHf5~)$HZ_3dC-`s-N6Q+#)5wv2vO(ilt8fB1)nUHZe zGZ5NH2JMuyvF#Q#7}Cg$PjK51byL^oc&WbO2{Sd`0Kde@UFpm-%q42M;q!O=(3h!c zp|@)qtFw!aYnKB&=)?>~evxcC;$6Oehud zrR^N~Jq~VHP#LJ)-5E6X6;>Ge7j0NUTNNgkOsQCGw_eG$X|Zd|Qq*3S*lWk4 zB$cfXAW5C5=`@3&p3UaW$)6Mh-9SNh&N+Ia8~SXe-o^9ndH1&CVeNdakAHHi$q{da z&U{|@@k7k(ZLG<0b;{FA$epIk^kM#X>!M1hwZEd1SZW4e1!Qxoc?Y5n82qBFt%X}d7;+1xKA z7+nOR4#cd}0`<{9Im`dluBiTFDG)WX)9hukLs=lM^sj+T6R&}Z@w$n@X`;0pcMlvw z^qmmG68d)krNi>Rz~tGPPyz4gSpD#xKL&-aT74V{v^tiMxpvb&b!2dx$J2BR8VF|K zO}G@cQV&$Cls`1Gx-D}l&bR0ONXxwYzU{}2MQl;vNa?<=Z7;ETaBfm}MK&_9k!&b} zcJ_QNSGmB8HW@$UXL7^{LZTqjJIOs%7oNRJid79wIa7~kN{Ifrsg_7o9tqhG}{iRhqeAJ2@?_Dt@ z*^$c1jji(~I{agBh={haU-?-wEG;Up)MB-B+tePmC_Sq=I_s>1TX|^gL(yP${FR!_ zqkwz<24Q41qR20{DBd3lmN;1JWQGR(6v%i=m^tiZoj$3z&t#UXv7e-kIkCP*#-Sls zMtTK!Hkhs3y)Z52uDQ56I6QpQlP00+>(H9*%31+#6>XY0jp+){&r5fK*XAQn@*^(sz5M=Ej-BpAkm<#;+{w zJ}qg|=(d#1qd`npLm;7GfTFl3CB#~OSePQcW9ztsjn(lBlmiT@%wwxH%3yUF-I8w} zGPzL~Xe*KX5Nug zgBX}{Z11#8#1>9G1_P0`_BXS0PK|GKtZcR=19ddljGrra`aBC+v8tFy(^5#q11I@= zy>z~S?B;+51Zg(ofI8%q#w;9!*s$87e2%3SKixA+d4Efiio#eX$3Yt)&h9(*{Ds&i zKA$faOHy{V;6lCL$eD;V$nNyBFGL51ek74EN4MItoXBV~@H!#;A z`;FHv8W~t#{$(PjNK%m2)oT*@R|!U$=nG9A*Rga$gTL`Lhbrk!1Pypv@p9G17BTqF z?RiVPo-Y3Q!B=G)j+0sSji^nwpRQX@Q|}mcBNz-mPQMy)9B69ouO!Bb#`e{)*ulYC zp1?oM#@PB?i9YGe7b`FB-CZz#Q*XK%G^U~^&>F59yKj}G*UD%dMuZdKQGtUO!|iUA zDF?;JWZnx2nB;#2*maQWLKA!o?b2KfyySZqq7tid_!RW67E_Ck14wQj?IXmB+qvL7 z+MOnH@e}jJ;|SbT8j5ya0f4t@T7R4vYE0jwrjBV{1 z&qKvny=sjJSxGJfdv8uB`DKn?FuGs^e^>Yjoa8`(o!<)}T}j11d+Y^p(w6p>q7-Vg zbslrmsQk=7j=Lmd^}MMQznengL3qHe>PW$(Z07eI!L)7$g-rN1%%_DoNZ{)AP@Xky zwQ*=Z@9QK=)+>-Azgv1C+alj;$ZCi?sUq|WFl2xrZ9k`5+=yv^$ZboSH9dZ4(fnR3 zXPnjIVwS7Ic>J-Z{yTyCk;eV>m$5f}8Rowih?O8AI+MN_%i`@Ah`K~?8AIRL0E&0r zVGNyqN$hsntu>xO+(J8|3p&0;74zOBtZ^TU%6CYv0zQ6e2GQ!}8sdG;NT+%$Y}OnG~GBrjrf0_P*$R1-N&SwbIu>_$!8P2a2sj<YH%rc2P3K@3qxHyOe#3QFp6Gt7L>L;&IbB_r~!KtDO zi+d3wXM*VMUdZR@Rv$i9)RxZ%Udq>ZUaW^4+*FdC>Fu<(D6=v>;wH4$u=ep-p01*h z4)Ur;TI!9Wd=4)S?y2yvomJ^l4oIMyI4&{Y(aExxvQvtXI{C(p@S9^UaOo4hvaCzv z`u?FFh@{~642(c{Bn=}F!q|y#W?;MyxOx~A!HW4WXeALU|Wdn2Ti z(yPjJ-cPy;8qjZG^`JkPjuTR^SaC8w!l`pIBwnvn8jxsaB{i>;vJB{ym%f6 zQ7nemF0gk9`%Tx=?`FS)(NEQR`l@d#o~3WUEX?*yAI&^{xUiZGKV?n?qZqqqF=l^G z*p7DSc75k80?`xdJzSj@_7ETIk*r7Q^A{Wxc-U)-qL!OEb3grXr9Q*2?`}asG{cQ{ zYiRiCk^71uHGnSz&r67K_-|MJ*OVNuy3P5DxbK`N?ew_@BghRl@J1Z1;aSFG3{n9; zm=2i;E<+XGZ@cQkBOU7Zk~HW2`19<}9XXTU&)pPr+#rw|kA8G`|3Npta&&z)>S_b? zZ@@+3aca8FZD?%DZ5aFQgsKzM`dfI{^WeZi5qG-uA{0;OgT%l7Zkv*^Cb+YHHWwR) zP0;jp+>5I4>zkT6KZaD#p+6}f1NZc{BY!7YaaJnBXc{5k*as!hpLFV=_B%M#$B-ks zKeq?+Eh==<7|p4F*QDMmy{CYuH~0MP669SL_sHlK>JDQ5s73@8@qO0ZIQ8(jIEp>z zLuO7^Dy1L!P}7J?xYL>%Sn_xAO;0g!(Svt9XFF;iIVMx>L(Vq_s(!8L&*#=myG;@~ z-)|!?2=7At+_xskCZ323>_sU1gTUL&@kVxw_%bt}`>Ao|A{%*@V99u9!a+Zo)(hP* z349otm~|6}@~=;3`HRYW1G~N*w;fr>Xm%<;o&joJ7+rtqPaajh42TJ3>0fYIu;;C@ z>fnKDE0}tuEi~7^CT<{k(l!Mn&BIhz-L@_r8DudzjxVT@= z34oioND>M|hWFpo)s(3sFYb15!cY9rE*dHA2as|u3TxaXO18@KTa!+w#H#C=-qqA| z?VVU-&~u{hRA@jmAP-Wi?)tlgj#SvPjPUU@Eb&WVx}$Xvqgimd3+y)}ZU{@ON- z!R9dR^NHDuPq`81aF=~^s7MO8ek6}hM=C@LV?8sa{4QudXKEDZ?cS&l3*4Ge-keX_ zuW^wKzZNI*^_N@Lbisr(hAlZ;A^QH;y7>hRfcCS+rcuOF0YCg!dy19wY~RzAVbxeOb!V$6bU^E*D`f z?v)0ICc?q*7jL-Gfd)R%#)S<{TzNMTU)VMdcf#52a2W0WoBj~Ewx}y=FxL}m6cVnMz@Se8HfiMu~CrP3#diNPvFE-DU@F43ARC!yH7`iBT(Od_j z)|e0GFFTr6M06;Peypobd{=~sFyUHKWUR=5TTwrL6*J8eokX*1CI2I^D8eHZ57|I|LumES8q-n|sEt_UtQMgI-(=0A`i zSS)(Zei4EtdPVAes0uh@4jb3@Ox}~`W&%E=VgBkKJ`OfwAfqa9FEp}KKfKcdE}Y=m zH*Z--F^MAxksvA0iQ1R(vTB~v+*7SI>(2zv;tHi2 z8ahr=#!YT(?vqhp$)^3Ra-mLJU9n~t8^L@WA}5A+t%WK46CKYJV@~O!KQJ99NR1Qr zavi+=QE5WMx5N7;vHWB%1PaimDNGDafLT1hdLR5J8h`PZ?~G~2tB?uo$=Nt36f^4D z@?T{Sl@i`kd-RGFIE#bf+DH37pM0G>TYxmF9_HzN{0{5p-(oek7V5TQK`B zhotNrKWM|Ox}{?!<2j2ci3Bk#cnbd-xv@blytSbyf|D;(gb704OZ-eP-;}Pm!CfAL zHC^URo>BbQC)W1MHn?qx;ODh`FpQtifqmZnfK=G9y?o;5uueYD7rg!J$C-8Z$Aq&5 zW#<_>zEA_2u}<-mz?xVU=YV?RxcW%~KXIs4uGEg(PKT3p)H6SaLXYeBpO!JXpL`>x zcj2Ve6;O8nBC`*5ZBftB*EV%F*EQGAGN+9g-v4$%-gNGB)=Cr}CKdZqffFHtJSRc! z)u(x}UVMAGzEm5QIz2W(SIeH4LxWddo^Oebqhyz%T^Tr1?>46)>tGF=paW1dG)M`y z^9h~UtG}%v?2lo=aief(i}i>f$Ch)^o70Irw6+iuWMpaMzgl01%lfWHkmr$#MQ=@SnqBsc-SO zuYd~y>KD$sL*QdasNKeR=fNvrg!mOueI|KOk^O?)_b)&G3iy8tHZy3t;(|&1ne)F( z%1CGUHT28HhQRwi`&6S7rIIjG@>}^4>$-SwYT2Xm*()H(VD2pSPP469DkR+|9$^l? z{}o_R@xGwM@oc{)Dnwgnm~}XY9!d7?&~F5W8SRBMH$9^v7Z7icn2oCe7!w>s7PY}F%$MuUk$Zja}U3b{>6SOm~x>Ychy*#GG=tesA9^*Gi>QNwm%P( z+R^S1T2!i1`qz_p)kwwZa}C<(MDsCWIffdld@|5&&F^I(F_}rG`0njP%!w|srXqTt zES_U8%c`f~A5~Qp_+o=ZFSl-II}@Vsy6GFA+JQEyv{b*|_(w&BbHw|kKC^$%{KS-? z{_cH0;1%G7d3RUYB{k0{s&^XQT3BZ3P`Pdy%ZHuBBxQuvXW4+Ixao6|z#TuxHr)u&^+*z{1LZ|$w6UeiL z+9wlbqn~biZOpcEQqx2dVJ-jAWB^W+58IKB_1E+>UWyib6F)R7JdoJ%mvHmtbLC$58xD*QsmG+OPITGJA^KXbH<>vR(v!(c~B{C z!0P6XnQ5Bu>TPtqc5k2BY@K)P7bFTFOXR=bvlJ_#ZxW_>)MBWcJL9*Wev6a`+piag9$K}Ij7?z7Alb?4CjbWM;ow*^3ePFq! zYZ)@g!cP?=^hPW2|s1(y#36 zM1bIYA3lyiB_v#Y3)HS}=o+uI-YJsKf}|Eu<%w6^7L*$w$!#;5Cvb!H{3^z%H4fKv zrZP+pMKWPDP$#xB=NJjg5v}0G*l0UL1jbiDI0Z1>K+a)rbi!58FRn+}5gC;_Q$FA5 z-lv|uL6*Du-T#dv73~qS)rOhnfI4d+{n;Y3T_>VqN&)q#|i=VY@EY7`2tfTZ>P8V>Bb#> zZrwM0Z8*)x$2P_&s!WRtad;n_fe3JSY9%-2nRyhI5t5TLjh1z)H=;MOe>~)SHG6WN&y?y7qL8sM@MUQ<sjtQ5 zc4QJ-rApx3KuL!*dv=m|Poncrk~u2_l+CTx3}Xxz0r`fz*QC@ukf-B6^@fM4HHgHG zIFdT@8jKHo&oq76hJ{JRV~o-?8W$a$>L2?b**B2zl=0x`kw9Z7-BjCc>~Vy)1``=X zKF{GxPL=V>jBAMz+0U6HccyV_2Dx1rVDY;L(6~Wo3M!9CFZQPV`{^64vxbVd+J`+q6WNSP?oZt=YtHMReaX(^7)`hSJ_aQGSuJ_;h0TxUov4470I6P*dugX7 z%<~3)*xg|UH*j2wDr1@7t{66_E|8mY2e^jRpH(#3;l1NsWS+^JzKK!p0r!p&B!QJF zzVG+PsyAb07J-|tCyDirP1sxJo-|xM#W402f%sftB0=lXpn^0u6vtikKLbBH-%WAU z3!jkvQA}UIop($WjYq5rQs}dYT}El9Yb?5;$_P9D7<;zW@MwtBq=0Hlv&~H}C%Vge zd?ccy3-jPAq_taHK`?%~Mxvg6|IA9cbK3_KB3>PJMrpNDEPt;u9%O=E@ z`Ew#r6Tr0i3e=wTfOzh?8H7nz*t`qvXkdO3*?$2wc=0po((lm}Rr5$j%<`2Qc35Gg@QQl+G&dsC5|fPfOCn~~BD3eqK@fFPlCj7h`j?iw*VM)yXJ z+Uv9T`+I$U{QLpmA9l90UDvtJwR5iXe8l~JdyF25A(}%pFk3$h14$-pl(KA)&_Ij3 z$_s{g^bBhHv51p%hZo0Fe6M(@xcDQgj(4PbSHtWBR(Bm3?-8gp6KG)rjP#-pxH%@< zqnG-RV4jr!U3)u}g`57qMw8K~c0xlR5U>w+PW>$a=qr0uni=~p+l^4C@4`^i=k>z0 zkLapR8lOjpJQB1v7KXRw=zWDi4dxAc3YU!)pylugsGYS7=X+P`d_5 z6=it!vVtXORg@^OObJv#ual|LfpFTVDtW;z=lNy3BkeDo zYn00TTTMYi7e|9fCuC|{H8qc1ggBxzq*guVX}@JjcSgoVm|RjH2uq4?`4K&#IzhK*`Q-g)&9*x{4N6XqFPG|icm zn=Du_C8g}wq5MHBwTXl9!KHM6Sw>>_F|jWUe-x&g*b%q07Lwk2UbiCb7w|Bq8$D_h z2YPHKH5B;$bHUd~-WgYe@SOXB!WjMTh0J1kk-x@!hYo+XeZLr>2-ke%9+#?(-jk7* z9(}nH1FrOt9F6Sr29S&gv)Id;#+O5ITp07VhV1_cbP=mf3Dhp12(_Ph(7yYw$o7Qg z9A-8VEbGf=LZyr$&MR|Z3Q6?xbqZ8~E$;q<8-r&%gpXZ0(uK&AyHj^hlDn)P%i49y z1JNG6!7nlRzFdU(;r-3qH&#g=Mep@Ut)6{(LqoUC>4wZ0N3`KH4EB9f))bQ(^g&|5 z^IsZ6i;9fg-2B`RqsumH3i(YeZp|DqHwe90{P;7K!ISgx z8{;G0De{=DT|Vu)I6U3q0vOt6pF?7wLvhPWQGiP+TC%?uuaklc6<)(*qoY0_JgXx{ zxQRHW@am9m=fXI!bZ&$>Slb|A(qqUA!E{9HDa40pc(V zk9x-(Lu|@=eFX9o=BO6fO)77g>*)4cJcKyb=-+%`tbk_RkN}8KnMnE5NmjjGt(|WX z$}Cj85hsXEQN~QkhT|9fW6zr{6*0|L&dx3e$>EC@;f(wGpV&4}u=dAhCDLUl-Q4v?WK(j~p9R0knl4RoRq`ms3rOj12 z7HKHB)D`gN#$6IQz;p^eI0?<{F{2S;?yb|uXNgH;L^EkYUfi`#g67Ix2O9ytsrFUF zEPh54G2drbkxwy-7vHOr+m28YVfyYGS-Jdt~Rk;-tFeC{jv-O z*c)Ip_GH;%nSJKWHy3^WqtaNM%Da%J)tJkT*@cF+TbT3r>DVva%##q0# zq~{Gj-qTpGjgua8C+)Z-ZuaIiCDk#=%1dGtg|pCc{2{3LK{LW) z`!3or^cTCFzQU;JXN7RlCMr^2QMrDk_VgybzqvVsTi5#_670 z9lZK(M?-pGUUx^X$Mk_~Bz)@Pdfl&_)X{exxR0z(*>NOwtz~t&=Bgu-&r4rV+*EBp z{*M3&GeF1oVd^_WW@V?c_3AA~bBYK4BM?;ajXnzEA3C4gMhNjt z-K?w`e@JmnHOlkjcc_!_a0gPM0(_*BR(Eop4FQ;oTa ztnVlUht`}ag>`X`l;X~LOrYje0lgF-kHCLD>?I1sa?>U% zA8OAJ-a>#mi4F^VStXGcN%85p1WK#do^iQtyZfd+PQ0r$242;d~< z0eU{JYZJicGOpJ0{R+TKiKAGzd|xsx*|$b7&Ez5)GKAbg7k%JU=0A=QIjxe#x{|;? zL8eANsvC_JbjewZ!v$y-g>dd^sbc@dkS82;sJk!8P<-~$ zHqatc?{rMR=*_Ku#9q4OZftO11oX>|XM#@gt^ z>s5U_4|JYUCB_-|^RIox8%JLDd3|d?3;zg0G%>$bH-G`1o#;k~e=tkzo@X|`9?dFX z`SYDB-E~JMZ(jM@(e0$Qhzj*d%BNBTP$Z7pd;Vg@Rc>2UC?Y_VnZPw8j(7D&!Yn&&6LE6DTWyXnR$c0w=&PMv6ssmLk{%61;1 zPhn5rW3*y%oBHUD4g{Leuj#Qr#-_jF>o;5&&{5?8s;_&B$p%$ThbN>8J#!9==2~of zb*@mFol*ieZaFVU)@OuiIW1Z%H=3ne!3tOs%W!S5S6e2DZ|X5xkpeZ2W0hD2P`IUM z3XWUKR>wc?J5}5MN2*@#>h{{nWACl+nT)(BpTSTeWKdJP> zsPTTI#0a8b>5y{Eh0Gf+TrltC^zc^K)naJ-JviwHa<|JKM>VI4{OCXi%n>GVVWu zQ=S|R?Oj=w&LuEE^VKa7 z=gEWQfH=!)<`0vCw+i<3{F$UeJbpe#L%)fxR7fz#CL!MdFZSoTe+CJrMd6lQzFHP%U3)QKP{RAABJIEJwg&fy`rT;#Duw<8_Hds4Zl||bNeTMBsf$(y~ zIskXg&l4vhyi&=HaqKl!Q7!0)cg8N;a$&Az zAK1<6zFAlksiC&9U=`#9P-Z0v@wju0ZpFG5?}CGi0*XOIKU)gvC6n6LHwPHs^#xl0 zM{r)H0t5Tnsbk1#_F9>~K;VnT!+{Fk?JCbn(xP$@jbzf+Qyg%Fm%UAZbTlM+J-R&e zW_jPVmT78FNIkQnZ5wAtr|uEiLeU0Iz-}8XF7^OYsik%l}NB;sp#O zwmJ^#JCIxig6V6tDg@6Z_`gZAc+H0g8hD(+xCq7b0`Z7^LUH6x;cp20QBDC7-Ok^b zFOM!=zreHVGSQbkBWpK@4?x&tAt<=_xckXmJD}&3G?i4SKiYaxUp|7ree6Xucg!!9 zUj~r5#Tyd!nZnx1v!qlv94uWk`c1zZW~V2~nXjCoTnf zW;?yJX0wa4nZ-1F_dLi{wmSbKaDUg&oYeGc^`g`(txD;%`p^g%&Bhqk9YPT(b634q zhmLQHKHq4nPdSJumxPE0#Wf|JJl8&{Q%1|y@sqWiQOipd?&aJ+lW_?YX2-Wns;wI_ z1ljyLY?~T=srBx!`um2$Edl7&4gLNz?Sr)QQ}!CW$qc(g(qavC_4^sSCiIl4D87J~ zcR(e4@hN7pFF##-*hs$h*{P^l!h%Nv|D@h_hxR@|5RD5f^VRKaWonvsWtD$pQEFdL z%BjWlJ!NjmR?R|h@$u`~t0=PX{A&&`JyT}YT}zHb?%f)C6$ZY-7OX9afhU}59c!x6 zG)=$(AnS&O&x8x6`wl6aU*4u;>?Z@;C)i})GFnA zemhCRl<$U|Dh8eL?Ipd$2HWdg=c% z`mXilAA;FC@vwITl+qwVXhi~P#|S@Ap^69ELG?T4gX{Al;9FD9*iXp|22-n($tSZ5 z=@D~wp2{MvMix$MRre%NKR%SqxlsB5%BLmb>9yvq){bNF&IrMol!C?G3`qEIJ}>O*jm|KEsK|AcXCv%(_8tuH?zCMv;!sy=R3gNlq3Z z?KUJb+Gax9D%E<`A^4@#W8dTOIMVF#AM{qs8uM5bfE(?!Ea2>I@w&p`Fik#SW4| zwO%r0WJGTt_M1DR{P|31`wpopJy#eY@D^ByuaLwF+0dFZyM9C6^dU^>fJW8Q1#-*d6Oyc@KN^XU3gvwUGVc&}rJ3GFYlJ(f5ad^Q%^D~~bAs`V5QR5hN zUZ|~`lAJHdRVJr)XLgw+rcR7|IFW86(ahHGyyPQfDm0P3 zIaD=rZINtD8D^AfYh>{r`m(bLFKun3h4Urvqm+ocpPno{y_Zs<*Q)sUf9{_{s*UtYLu%^{GQ~sz`m0Z^HiR1lN`VDRbmbg_ z@3Qb@r!vlJyvf!eG&T8$?pyj8UPu%^Tbm-#LFh~p{0|j~NKod5AKe`FuMaNn#Bbxi zrtFBmzE$!9JV8&<7!8!~^$jARV+7~F+Ti@EZ${>x@V+do(jvz?%rB4KxEkSo>6Dg{ zd78^%^Es$Sn7po+tDE(Ec!0(82$8QOH=N$rljjurIC{En@1)$?#<-ug;hNNGRXK{s zAGk{c=@GjVFH`leT**8+Zt`v+Q^jdVyZ;e%r7)VUKgq3tE-WuV_b;$z#;79e>rI9**4;5Z7hR5cw^#39`ddtjm-^yd z!u1{JFFRzB&E!${G|4=C~R8#b#i*-uq8mdh3RpBy9`=NwYl z>Lt(wt6s}z56V4S2pYZ++@nvcHdZT|SFZE-43SEwq)w|A^C`>VI{*7_UgEoylxu3} zVeebDoxFd5Huaz%)xIlQvJT-@5y(uR(I@OnR*Tnq#$vTKTSH3W&1g5SNpfJ@&ENRx z^xOU;acs>%hK8+P_9+mv7oSXZqf(EkcS+vvh7yvR1VP^fxUT` zjAoR0d!ccQ{h5MzSE1++_>_?5IPJMy;saJ+=s>wVD|+T;$TO`so0Cu!_F{?33Ws#a zTR&!S@Gp1kk?6F~X5wR-=^gQAF!p@r1<0S_bOp5J($_qAA2aUkw{dgQbH1l~QCyu0 zQ@Zow+qF;B#3rKf*OBd3@*lFWC3SeVZSGu9PAgb6cuiW~t;a9*yNj!5K$(%R6~fYN zr9-|?R1$C@HpVfDfCIyr4zsh$jdv-Y4CFZ!J0HP>G-*X{;_j`7CHk*Rj65(#A1An! z7aH<3R%aeJUa8xKWv9Q0Qc`LN99LULJpH?uVN#fH5hA87rEZaI6w&E1zCMfk^A9QU z(rcs6e1NInGMUV4UNYwgTA^fqIi=8EYS}5dc79lX*7D2RYlT>!j`f(3D6fUs1xKSH zE+c~q2I~@nj#Coj>>>{0`l= zYWY=}4;mc2jUUs@H=p#*M@lIX6ze)T_7uWua zyKFTZS3Y^zTaSTN+z;4NcoXemZC;*lxlsqi)s<$^&$^Foi_dq@-JdBh0D>YtDu^Fx z|LC5XRVPfs+Fy~UlQInAZ?a>~kKz}~qSV}6-5jgqZRJ#r-!tz2r*e@#WA0J_G5&4w zFE1e%p@X!jv4nuht9LV+85l11mcgJJRcjzW`PrDILq6M+7^Q)XE$w}}lD;j~is~+B zj+OMZLpGCcEruhMk>`MJLi2f#ZXKe+HCh$=?!=B!Sm5wKf&`3;SQ8m5N#A~u^u8$z z9PS7`-SKXiW%ypIpA*aizwehOBJ|KpetnjyGfTCYb7g%C^0J

@j-j_wHniI1hvG0fMq|m>SNeahkp;f7tCuk_;i0|)dnBgZ zE%(IbgVaM3OMYRz7wh~+xQbMszeI(Wey`Og_UIFjKu|yioobU|SeOYV6$svjK8h_i zK1y9SeCnKJOpQ!ky&(WHjiWE45Qv%MkBwiHYG9B(#kmHy@KH}B7+ z#k$ki*+y2wP6j#UZEoYyZAnn9QYN_rCmryQa7qO~L7+Z~f*lw%w#4p#N}@%J9r=|% zC##p1lZ5bsCz2(FYn{zBF?_RolKS*johUia4mFDhUu%#hz-V#U(|M)*J!I(0hu1W~ z$8QO!o{iF&%XyD@w6bEM&LO#7R0+-zc`V<090OyBhCy-kON)<+ywIIyR07$YuI5*iJlEO>cc3zNdA|BB6XK`kQharW(A$n_7LkfL4o%sb2c5o= z-r2`VM1$3Zgof2h`2XOw?do5nAQX54_k;iS;;T~#KF=i-|52Fyy&Inr7VT>O?^e~u z|K;CY3$r)DW8C{6unHJQ4a6<4BMNG+R5V7BRGqrz_hB(Skkg5=lq%2&XOYgD+p-TWv0k8hQKn zw79M@bU}I6w6droj%$Hb^$`)dt(=wPwm)&RmIFU)*=w%U34iI{i!-Nc%Em{@{KD4P zVwzsFmn)%c)QP%SqcVqO7PM&=9t$P1PdyJRaCmshZZkPAaJ741_1jSzpY^u6@onQs zz-C{2+r3@??U-#uATQBYy*=DAo^lM7(@hF-75v+rT+r-8-4yYo>Z<9t(DExIuRzCD zvuJ%&!k9h6@mqz8`Ndb>ktPU>rukKK(MD$E4m0G4!Uj9po6*+IPP6Pdzd7DCRS|RS z6>p{V=|gF0%>B~RFZg)@M7+8`3R=J9C(mB1^y!%r;}PSoG>&kdFXDIVN|#WUllI6ky%^A(s8x?8L0`U^3RD)?BGW+eB26XJ`~&?Zdh0Ieq2xN8 z>XnPi;}Of)ta>XKDRK`q7p<(;Rallcpd%y_M`0L9y0-GjS5dhkv-|o8=6aWhs%`MY zD;*k`pD&P?`dIOvWQ~Ly-*zM2alO@BYxAW9A zha&f_*)2&)X^4Z(WCmFDsW4XU8479Pfn;lDkvO_qq=eREB|qsI4wdZdBWaf{gJECi zQgTS@b3eu%qQDg;ox;hMp5Z1UT5Y4J;k4SX@gM%5FZvSuzcue-L297)Zzq%l3GS!< zZzH6j@SYOgpTG8(Ocw)K}vFXRJR5$o;XYc6=M zdq%jpulSPm-o>*4w-;Od`kQO4E3AD5&fp@>PqcVo_>Dhfr##{!uB>zx7fxD19Re?^JTEyjaBfy|+;${?{`hg_VoI3EK_qg2zS!3R1!^pexV(hBt~7{-5S)|D zc0|ycew?jc)tNy zYM%DG3ma7lS2!5|__T5>jc5%&zK@mc+*b+fF%^nZqE(?!J@Br}ZU6l6Y|MZAJcr2v z_}FVG-OH2y`hCdDZxki=d0*>St|o6GP}4x&X^z&)wEoG%AAECR;nixt+6AEMseKUfi zgL+uD^3};6F4XWd-rbT-W0pvYU;QrM6p6T=+lT|utp@XzvnH5&uukyxX<9RD@X z{if)n2&j+nT=}0XJ&B$5n;3p2A+O;!*bDHcx3#vKTiN%Cf0H#$R^Pk60DH>*P29@( zVFcg%KO&RPlWuEF*(3_t&XYPTV`BeG(oM+JH42vs7kuNG+;v}6HXMf$zq3Md827?~ z;g(Fm@Pn~qYu6UHJ>$!G#eyxt)bcQ7E6(X1!i0^ynGG!O8aBWq-+yh4O zBVA+{8opN~BYh|zdE;YId&{xSVLDCHtLeYMPIF-U<(EqAGl ztNB`^QUr2{tJY2ljo(ussgtJV ztl%E;AIKA%456qEuk`}A48pPyN69p6Z!3X}-}qw1cJb343gVMHi7@gKC)Qpn_y@;;lz1ge@a3ul46lFBtnU4CE-?hwW$FX2rS47 z`0zVTL-}S)$!YbQ=5&t4(%*_t&@w(>pZ4pqm@^p^$S*&FhbF)3KZ3stl*3|+RymzD z+Y|EqYfDclD^ZO3sRJ>!tudMD#5)CJ4e~cgITVl%8~JHwwRyT$uWn-d!+?vC!+}NU zACf{1rBzolgC%UtzWO_(Vw+(dH8;@`c^Bo6JUqGQf0T8W?mC8(f|01S$eM-zwlx;C z&-|7$vr8=lBnM{fS%ghPg~VI5HPlCHSC1cDX2ssmH{;#-Ozf{0^6e~kKZI^!_j(iB z7^*{OsrJ*#c>A~Tvb<-7qfwapT+y5U|!(^>~Oh6PFz1xe;j~fZnFsX5u0~g!os1SRT=G3ahY-76zK1H?u z+YuJBW1TtlU64G3S@0Ea0K_FSpz^KXceWx{TkG&QIU0S! zC|Bn@K8f`^-xqoMiI_K*=Ml;K0DXSg&ULkK!e;97XXs0rQQ7t7q8ZOa=Vn9i%7T97 zCO*k}=N0hi!2IS?c2WSw%rxfwS@x0K{wMpx7>Cq*Q>_z8O;ab1(8;r)C%wyDb@i=i zTnVE4hbXAVDwiX#K2$8^|yc?OZVNcod!jKpLvPEM95ekK6FvJv0&SETcMj zc`UEwM+EKJ0Dr7^bz_N={|y^pyVnoMUoRxG-c{nSSbC)EJoh6oU^Si@bsS$yc1YGk z4IL>lu&#!`4ds0M%j3NygG>U3WMs^le*pBll*!d=lQ%(lgI$=T>E?8 zH7YO0tEzRX9F;01R>r+$iJ#?S0Y_^&ESoVpE>j?!))8JWYk`%9T>0Z9G zZ%OC^E2yhxmU@Qs&*l3*R#js(^HZ%?!kW-^<|c-%P@ z5{pYsi7Oi)PzBxF5m7Sm_WV)KO2NCk3F3E>8A#sbQ=!x<&swF@j z>3JNbCs_wy{0<40K8dQHQf&`8iU!UDYM&(XRZj=M%s;vvp$O?r#j&0^3wMLF^2fFo zhF%cIvWlvlvci#JzMTsn>M<1C7kREtK8x194=B5>;;a~Z0(`;)6&2!_#=@Fy$htSH zI2ArMsYkXo9DfRaaVwQ!#9n1gD}}i~qs#jrfeE{D5S7F6pHI_?|1zhYw>NPr5~?qO z5V|5Wj&^$q&Ix&%W_MnF-O!?9iKqD=enk8jcX$oS7snfDi6@lsLp^>JejmTDYliPK zNl*UIR&{VnTMaL<_r(1qB2(gCzni?){@>jn1T{J?z$IV-bK<4e?(6^m{Ps-d5Fc*V z@jn0yazm|Dv zo$jt*O_xcsv&B2eswKooru^zZD(9m>wAz(Cj>(yKULO&hMvKp zaY^N6f-vFA%G#KqwPaQNB!L|DUeK?3vZGs#Cz~_xH`)bWv!smchooZt*1Gvm95Z5$ zmJ%#tZ#8VO460#Xz4gVyx1a;!1GZ$fTYhc)G7j=UIUD|Mi857j;~C6i62T1|#^m4q zkhyMYk&C`L+1!bhm6rr?uJ{!b)g+VC|M;9zZv*&tJ!3OB9kTX}qV{i-cee%LNbcSO zT}XF^yF}HSyg{Lw8>eHk%gakkxjP3FDMS1<2vL65s_4cw{z8IhI|2~?^+4nERbJje z^4{d^-oFrAuxcvOVWGh7q|bvJs9usk;I^7Rf8n*@!>z;YM?&c1uAso13Sdas%+Zdq zaw}+;A5gv7-^usSs4iSF_B{pHBZ}%AfAP4{RYnD3D8B>|u?mjMUuOb{u|W*oBNi2? zAM*yU0}o%f0l?>ST7lP}U`)5~5_`bq0QP^?{A-zJi$-7=xk%ry9Mf|cM(8Y14!u(XWL zNnJT7x?7|#5462Ne|L!RXlZLu$t}|{>>&EpSfvze@>5FTA8@bZsC64r0wMEQ?M}Y9 zc-4VZh35Rg!$<-asle5C1wFwC-w1l2gcl~z=&QLql49J?!fvWbn=f-cLe;fiPm=6U zD1)m4{6Bi%2R-*bMU(X>jnw3X3n}~q#!@gZ> z#j{b-&zS|~bZGb5WRnM?>Gl~}PXds_t9a{@2{zLpl`M~i?Qw15aE5z`cVz

p-N6 z2S##hNDuFO|K?>G+dd*q`}3wqW>Mep2;0sg3TgJv zkls@8vWAVQ@v`uoz>gn^ns0{l8aS>qy#Eww6q+gd`D8AXL`ipUGiq$M zXpfMP{vF%$2f!X|&A^lgn^s(&;ROp_#SM)ob#N zW2u84<~Ou?7H_IcxROZZZ)>UE-QI!(i7&B~&$rgDm$>M5*N>2kCF7&=FL6;X5V)nhA$pj*(o&^Z-=i!1Wnwj{ch1qE<;QJ{-{VJf#SII0l%)cba?i z3id#SJ2M$?^SFj-0Ggde+Z~o3NpW|)8Qdv=KaPI+yZ# zQP9Ou@8UxH-zQB8NbuJD zshNi6qBwOAZvvA4q@HcVj;?p#(qU{-o%eG3Zg2==7xHiLRps5rCKz-Tgs+*b>S$`= zku%Zl4se0b4oKnWAle&3wF7raVlGo^G^cv;bCsfRfi9)O_of%eiKy96DlsD%#DF$M;(dtw<;^`` zlvtDvE}omMwsD1Yr)8}1?kbWJ?T<)d2Uo86vYNMZu9M%0{rA8YQPpC#AyKg9jq06eRkc3GX|X_|iLbg^bUABZIBh%*)}4f{f#G&tpc^eh-{gsBO($N233HvsoF_!1!EY%6jWEKx+S=GpGif)b z8l6u&?sN13kM%8x|47E($S;prm5ku|XJ?RY-k0qH@7AGq0%UrdC4xx&P#DR<1RvfI z1}b&4OG=Ud2oSUe8ATmrLlg`L<4YhH#Mln(9HKX-bX5=M)16_R-7p{$)Z&0!2u27 z$;NtOb8T5ceE!{FjA_{|^g8K$xQP1JNi6JGQ|lxLxRpQ#o<4HsteS8+?3s;_piJSV zXlayIP%#ds8#nOnJnlqJfUzFBk}I~%T$PP7F}6ToyFX{Q{Npu=QEWt*{-onSDeqJ7 zZKZgJ8cc26i@52^=ti~@%sE!#QM?hgv_E77QtT4=*?z#T zE&HlcX{h_b+f1BcK=PGIL)-16<4U14=e0Ig;}ClVI2P2MBzZrjZf5VTDB$PzvM9Q) z08$Qj_{=H=7HqU8xs+lHXU>%njukw2eI!_sSv=c(ZOuoJe)4ook5ZNeGgjaYAjAA@ zSq??}ex+<&o9(A2Rd#_c+@&nuGx?n1_KCOE9`ATU{#iqIO?ORsiah?Dyd+_F8C;M^ zwqMuEJ1J13#n9~C`XZJ?z3oTfKq8#;V(dLqd(H2-`OO9Fk#CYlU9I&3^k^k~yO7Gu zbsR~CT8Y#umfcG-gW_U&#)vD~m%n+dkyy&=jYgv^F!wppBU6!D82X&Bg5{==zJBda znd9tf;j6s=2*|CQKsXju>nmTba+j?s^SJV)wlwcdw+P&Y0)-z02D7WtV)XS6IeaL& zf8pjQbz#2wOV$Ojeygy*`5ytDuVv5!(*PpnpC(_!><2z;F|v+1oO1wOsUS$Z{go|u z_Zt~^&)qw}r*PF8sey~>q}7hA7dOMRSIPm_>lu3$fr6IFU@eK7Iy`l!v7!2ujHcxB z^HROtS+PJMT?vHk`J|JCpafUi2NhB$+YkGf5gs`|;#^3I_Dv{%6`3wB?Sd6rlIWAU z2K!xwbVpux3q&rsEPlDCuy1 zA6UI$;#Zl@`Qy;=3FDAg2gOy)b}ivoVOt~H6#}cu7A4PY@ft~{e)Py+G432UPCmo0 z5Nlh4-xwspqo%Sz0r=!WQ3eGbvIEkM%_o1X7*^YA`T6rATVzt)Ruxo-+S}UUa{ia-04{ONDZIeQea+AI;k9H=-U&rk}nbapjTHPfPh zZxgdVEcaT!eS^JacM3CNJr5Q=nA@Dvuj;wsx_#Y%UE%3WcP_WVy#EkXsFMcV!KnNL zY^AWidc8XpV;34?17!8xMlr4t{H;TDpxtFicvx7b0#tq2 z1)A98l!6FX{*CppV?k$G#BxF0n_iBF6$f5M&TqIY8w|b((SIZ%zsK@eY(ZKgcI|kM zrw@)xTU(ehkpkg^#@G+C-bv0SCRe)4|3Oe?qDXw;YQUwxQ}f`c!+=ZvU&yTtW>`1wik^`D(-5AM6BEZ zw@w4`GRkzfN#=lUi7oM3wU|DQT*!=wX<0?BE74wi8vU`jbtmW8$P={RgVu-8w#WOY zW9x!!L6m12rTNrK@)zw<*6H*l4!;ANR0*wE9mn>Yu|dz!Sy_FtTuOe<23*OiqD-cZ zeYsFb;bxJBYNnclDds={Q? z-&7^H{!rF)*La%$#vv^=Zie>mx92p=Xqb(gO-b|QRchsYqNwh8(p<U{(xP_dyXnl+d|17qo{`iU~-~9R10S8{O|# z_2CUkMom_&_YT$X3~rN%n=aZO`O!<3>%DA+@WgG}cp$S^Jz<*YzlPW+9V$Ii+Fe#C zXRu_Lrrz48fT4utpFM%k#w!9rsE{gCN$~(1DcP^-HaZd;=Q|pP7#xhf$BdQ)vGrK# zA^l99&gT*)^rM`asZ2(=Yk;N$4==Ra;YfA{_BLZI=Dt#9IT)@ zLYJ(a2SOZaMgaVnkYCmF&b%1 z^f`XuZT6Yp43MAHQL0w4r`(C7!lw1L>#ae@eMoM%QgMS@C)cw(RmYR4D@$u>k%Y~@ zeI1j%fAf%mf<5&Wr(bU}Hc7@9akPNeGc{MA)%aH+2NcAmeTmP#jNOK#Qrw@Uglj6b zRUku9Xdt`>lXIR$<9*bJ4~~{Zw|n3turS23@+WDAkZvj;9{7w0ctIjE&7OYDxa9+w z1xD9JQpI}}*T^{n zAJ}V?#S@WS+n{o=*c5E0kGTSWyoefYlF?3;!1c4S?2&Oi@S_3uQ8SH=uJVTS!Hp(= z-G2nC3f^H83UAI!bb2);^d?_>WoSFU=LLwYaRSOj%6@ zO1PUNoK9SFEwa{IRWJ|c;euwJ^IAR5uY4oBc4o#L7#DZKLJwrPfyS2=Lj1n9M&DxJ zW*&T>r@6$3Qh}Ei`#VHN6Xz;T(@*p!3xb}XZFMe`%yR?67B6WFSpD+yaP_=^xX#)s z7xBgw?WsIGV^685q5ZQQ^$y?CR=To8)I>#vUEfc8zgKpD@RFz&+K&8N^9cT%C=P_Y z(PQhFdBr;ZPFXj6wj_JHYd$0Y9I8-*d4)WpI@Wu0(!>5AL20Sz)}$>+?~FnS+rgP( zT@_RasX|Kmp%#!3oEL3fd_FwZFizImTF#93szNIlKiWBC9fXI#7vcs}2CKduw{oOf z;l_&3=;nNt5q(`aHZyA9(#IL1ub50z*SCN6Vb&*SqSaJBs9)2I<70;1)C_R|ABLjr zjeU$w-8eF$!D6ES?7Tuw8{_f^g8?j`vx_w{aCJnt#*YuYe_)ScyxVd_diMtq#Kn;GT^rz`#37R3) z%X9P@!cPi+U>xcg@_6rq=)9q7*JtIZ5{^nB|Kp%Htd4fNSm9y_8}+}s8H7olca4kb~z2;vf?=H82fe zn(Zo6@rTEEUkepHQ6r4W>sS4!s(kj0ve~j3B~w^EMUCwm@|1LYF zua&{LJROH`-H3pvBQsC(p6COC7orCz)*+Q! zRxZh!K$3J%jhs@UM?+v=0IL4UBI;WnE3=M9aKM$9#B~Di^v5j8{0j}E)4J>c#(C%8 zOv-;fLG}we$){T}>n`Hm?^krNA4Pcv*DN58GH3Q zYEWYn6mz-K|y3Vmvwnlh|zU* zch7nyI_?@MO7X+3({W=EMut>3Q1G~BzD<;tw2&)LJhz=0pI>uW*< zg)8c5;K<0$8&7SpIW^hmeyYj}VGa1>#Y{nUlkbbLA5MZKOJi@l{aN%CS1FvEsn?}B z+8)mvya&$&Fe-1SO05fCZPjN?F71{|Tb@s?nt!F zFtcKSY*z5E7LVOjtu7l}U~pY>-8ISYJL3bcsK@Cqx_g4R%H+^vk(mE=%jZtL%k$TvpMB$?h1kve*XsE}KcEwyOTi zf-&vrQa3+&f>wk0_18kKY5>;^N35!--}lju*viq5^$xp5@EG>FHB${ER=ct#PJ{gXJE~GLt&qV3U^Xu9? z!2!3U{BN5kUA@W}GeIAhWXbcP`{$*|$sJ^xNB@A=gXXyIGGdktvquo5D{QuEez;wg z@||_eSc=lrJklFX7~fHb zRTvK2=I=hOb;xS8)fbEn`0M}s`9^!g1W?R@Ib|(w&*LX<<@bb`Y%(>mqE*31Xv&up zJqMKK@zx7J>uC{+rEUKH*3YNp4qMX3ud9%=3|0N|XE45s z$6OM>0BkMwex8yiqT(`9L1|=?ts~%g7;=|A8w322(^tNyYq%7vpxJ&ry`{10|S0InlznC*f4l$KD`>zcx5* zxOga{cZGda$swzs49&N^WGG$l%_tU7H=IQwM;6LR(jIzNC$RVQUKYHAa2dbxehOMa zIl(P>xW$6y=yhO$B>rff_j7Kjso%JiD3lkMgK7dk)=2Qt`fyZe zbki~#2b5aNQ0`>nRuUO@`8%>bib)O}Iptyw@Swus`7IefOb@0|P(VGphq&w21kK7<*R1Sy0xzd8wsl0Qy z*Y&0G<-Lu15v({v-U!ywHd&l~jYm40Z%Ez)AErOs^87Fg*K1b+xFWlziWB903~>(o zSawb~gkI}9RjSbu@$UK4xy1uW=TF6vEFLdw@lhTnDRBSDi}FGlN=-~S<{U?+`I8-8{^M6B z?a3l0!uu=Cv29wdzAv$`^j-17vT}Lwt1~}&w9BJ@T>lpOFJ!9%o76`E34R&K zw$CzTwJg$^^qHBY>R6OR-Z_3glRHZ|xNz_6h}tp4{%rS(WQ!)#@aNWqKUIW@(cxnI zutjiueEeWeIQQ?G=P=zy8GY*>|(FT0Nd!`CL^K5_2)em~jB*-IUqGTD9lC>gf8c zvSn+oG~4Q7+X+G@JjYg7K^M=?VkCDKq?v7TfZhw`n?Y%ed;+ zz~zrqEmE#v_bn;pQ*t_+Y~;tV1VOG2BpKypubkyM#MAQH0FVp9(fPnxKTPRnq^U@_ zwd;;!0)(L)SW}42teLTzIrJsK8yd|aKYY`%&B;3xZLNNrFHPyIj7>OhUW+bWBRozD zy%F-qNGZsO!d;Pr7DNv+mo2^)M+RsG$V1rW{fKk2rq(i0-N=F4`!jdKzO*2k8_9KL z5EJ&T;Vd@NE>12FLa^7Z4oO#J81-QNGkYpV4(I7$u*#omw}S z_r6|S+L!OTP*2jTQ_^WeJhJ2_A}!!l7x+mi4jT=BdDY@miGfvN+7AWL%B!F#X|la6 z@C;C7y*5-B$p`g&<5@{;&NFuhbi{t;RD%bdC&&naWm^Z%f5P{jW# zHpIh~8~)!O_dLS6z?%1eX=)YWA~wIYa8KdCIjd{q?ojkL!Mp5E;kfk2Q6IIp@%@?Z zNQ-D*skcHsF2sdL6p~Y*;OXNz6(5)h+P<(qLAsiWn(1~g0ODfybno*4YT|g5V|x&} z@@$ZKdZfgyZpkd}A70=kEx)&4R*IE90Tj>;6RF{yH$7$xDd~|?(1^b&RakA0slye& zO`=U|Y{y;*8mgQsurg6PLHE<9&$&ZIQiwT}EkelOx4tgzSgUte`7-A=tO8Pp6WHQq zdSzegKf8E$0J(GTo0>{(9YNGINxne=$X%WmuuXkbhu}Mxl6G&lCZKtSYT!I#@a({?kGoe0@?mmo z|M03?H!64PV_QdVv|KVZBVz*>&dM@Y(sRyCXu(V4%ZlR#lV$AJMho~1>~GxE*1mu~oY_0}EIq+4!W2blw#P)Qq_wG? zGth#39w^3!3dg=IVxU~!qJmG};pEvbooJ#Asf%xID{dpN?0MO9q5bSo-J)$@Q0XOA z+>%`tQIY-M0iu|6Mek47?#04T2HJudUOtjZoR0h^68+-m*#pmic>9{*)bjF$<6SW) zifLTWL?#o>kRO`09q9Kwbt~5SGi7T*!t18+`K{P&5GdJu!^aI*FiDgWJ#kcij$@FX z&sOr@c{XYUTa7;HZnFbFf_**Wt1D(VDi@dbm^)<)!Kz0u-`ZJ~mc0E0ABSZNs=h|3 zQIl;keY;xuQo%^A9=z4BIL1N!ZGcJxz@BFw*a*eTBh)zVmqUm&H;>ZW#a>c1*)DDj z_S)4k=H*w<-5n42(`Tp4*?U1_cYXF z$sb;}5t>AMRl{GWObjN*+l|4fcpnqk7&LJm6i%M^N>5O>O#-wLoV(40oMgL zX&r{FJD(BWbZ!Ns2_)ndeG1gCw|^NX--4(GYW#^Q?npS;}s==H#~uPxYRJ zFy5fMQ{lD@C{aT5o<*BQs^p=D^r%z$=5D;g0LDD}xA%-A0o;BqLcc|PZELP{_%ZqZ zPuPO#&~`(=;p%#m08*`_ghb&(n6tf|!pI|yxY>i}_myDk*n7AHShpYOxEE6@U3Z&Z zC^bAkYM%~t$Ts)|AD`Ut4o3@p9VPj-dRGuIA1ii(@c|&K>I7ojS7Ayqc)QquUuHI#$C?`EI8y7wq2Ax##M? zrWbt9tEbj5UCXGsZW=7V?7|lBhRLm1Y;CLCk}+r#qJ9$9m&n-bkX376o>?EEI$FBp zu1X`JZB6K<-L8NtmAW4KhsSE&!}J=fhg%Yo?zJ(D9m0+{n}YQpt^qO$-7HU^d7}9a zBBOH}M)4s3@Jz8JD_&>Uw5wC7I-==TpZ(Aapiyp=2SDxItG}_r{9`Io`3>saQXh7y zY`_3rlWaJ{QSy?L3>kYzFeSiwd0@{rw@d$T01mkjRQcpr@t1|tQ{m;>_yixae%vY;)0lVL0Xx&4i64}Ov_!9MX)hr>GeBf{NZx7-F=OevO z*uXVhw|V!qwQ*C}r%dsj+pVcg#RbK0WJla3KYgBZr$+alNA%C|I?(dIxXgcdk&U18 zeL=S%DNg~-a$sC6VVMxi1kpHoh#QXI=9-qBEYy&E;L~Q#Rk20EJO^!;;ToR{WBT>9 zZT_mdLYG?Sp>d0?6cSL$-X$*V_q@G&iLG+?&)HvnjDo#hSNE-y%D7JcRJ;K<+-8VS zj-QQqzB@h)J8p|*oNZJt_OtMMQu1`ZDe+OO6!(vb1H#_4MxN1>T!^cZQ{U>}=SAyV z=dRJxwQbz!FOc%+@1M79y!n#Xhmzh1Rj%{+RP|GCw!}P)_)$F5-79Z+a#G4SlHbzK zo0J6)ACWFd1U%FjgoPIxNRPKw1?)~6wT0P$Q-2}+CQczG=p&Va%pBU<`Na7LdoL_Z za^ZiMJ^_|S0F*%66B@ zc3il6yoR&t?wbR|S$oR5zm1M_YB9}d=2b1sa}E_<^A4+2VTP4dnmLP5^sja=Wp8c? zpal;8UJ}Dy63G2R#%Kk*O?HGsS?0P@j7OG8V1n|TSDf*)M#459U3l<~PZ`7@cls;e zc1^V<`9C~Mxxd|W^;cSLk0AVAezYy^6TVtLo>14~k2SXOit`hH@RYWTta!w%uOp81 zZ>>d#vUMv&UY)JY@UxjorA_Z)d`s*1N4>S3k{h)OgMH(CPNhUAMn^8h?NuL*6ips< zAj&z|)Fdk``4?UukVx;|&E=L~J^S?e9!KiCCNHg=%CobIkTv_6;FcF?rtNnKcOlpw zMPHOuk&NG8TCretg2zOs3~mp6)6y%YU{`dU{;7 ze>=+%oPct^Z9_9mYhK#2BNEir{*V%W-NKcIJiL)d9 zjNYf=fAiUL=68&(OB8WTpIZLNKr8xTpU<JzuF)`!^V!$V zpM&)?f_ifp5{5Lau`NA5e+ zOqo*&HF62xbelsn#gS95e(~0YjPh;ccs+#Ou&2Azx*ig}Ga;PLn4sWu-yZgc%@EZD z*&Z{3^Hq0qj$g-nslb;-a9s*VBq2`^se*WgLeG_DR9_97hMf||TQj#` zKH*V@Xv7Y3v>;FPi>~=haMnFNGi9U6D`L7t9i1=MdWp&heSg)4%q%%;RF7IzaEe}W z8^aDp-AJDEI9|p+FM1K!De#5h!o}PC&BguVUC~O2o?i6KA9zfM_riy-73(xM1FG5yqP-d-Tq=YO7p7{7 z5U)DHgMvmijH?QEv@!Z2f0jnuhaD{2Q>DJD=&!$?=-qU}*!oitx%StH>SntKDGjY!awYss$Z83|Yj z$}IzIGtDz;Xk6f8fgfslBd?w1eaYSL2XG{TWMT%2^}CN^K#RceD?=g7FP)3%7)185 zRt&p*la&9T?I?DxJmC&mMcclg6ZZ<@eobiJMd@6F^9vn1wQB4bn61#ePZ@T=pWv@C|oM~p@#p73gS?Ac~_&WFRz$O{DZO{Z(LM;9BO2_3Tu5ANC&@rl7o9|s&xNq))lec zGWJ!(KfE~blW>?N=o0%6@5Jmx@M?y~Jq!!^?{KhsXX8 zkNN+f*#Bc!%&3P>t|C!*_bY0vxxTet>=B~lg?mr6{5y4&M4zhbQZ?AM$N%KUEpX~cqgt8)+%{^ONF57Q) zp7NrsnjzOpmSB#dfzLPyC-dv`PUiJC0qs(=u2iKNpzLQ0af%@^|0Z#fyk{;-PYgL- z;b^M}rj%79UT!(-Os@Gum$1s3rwYRfH~VoDxoPh!+uPhB*7yf#>j2s|^bO%@v9D)| zvd>!cV7#vqy_W{5JSBwES_e(i8*AA!SRG8ZZm3W zLz-dAa*{tM;FjWK;rdm8e%6y4?(yH#dBfviP%->&EB`!XXhTxA~-BHT+ z&3q(JQhZwtS?rX9t1@jNfV;G{;!PfhWr$CyI0(6TQiudjvtu<%cV z=M_SljR?{NTXUd_x%2Dmk1#Us_zM^5^g9uIR@{oEVD2B@7gKl^E3`nN-UUF2y5IgW zt1mYJ7BqkI>SDn@VV7FZvyHjR2F0uDyO_o>pwy?}Z`Rce`f(+QbyS|lgSZje5Ei%V9-t4I-*_` zP9m$HrHiOQQ|9=gkg~NK#4J~Ii^5amNgDBUhD#&hJpGZ|G)UCe$GnW7fx+$ihx$#a zlD8^JdbOM?l^^qm+?n0Bvlr}On6P?;17BUKW>V6}&q`zVzV22R2d_rw_zv8LO~h1E zM97`q*SzCg>bx$ky~5Ar`gSdjIygKMNr-h<*0Zmajr_UPaQJ;!eUZ&uKE_!pVc5Xy z1Kcn+js1(}TOD3R@s3Zdrm^v^N0N(i*s^l%xH&IN`xV^LvG~xm)N_55&=&AGJ3>)= zrULlczJ6~{`Ro0{S2|;7%y9zyV;8WD5m$)@ajonhd3!oV6G~r`^f;@POe)F`JU=^) z_izolKYva+zj(>kRBv$X8HL08w?E05W`W~8Ea%M}aIkXdaS^=>P*v*jVwT3J_eQzf zFem^-`s`zkMjp5cV__|CrT=9VFMc7?b>|f*r0qGpVe)fbgFfF4f3}Q+1G{PSOu8TE z{l-OqvsUy)cm>SfN_oJ+qJNf?Mu2$*Pbwr|-XvbDQ@7MmD?w?}cftkQMw4Hy)r5;78Yh{BXkyR!6h75tRjZ!<@3L;w|(pwP$KVR7eO1#oQ6nf)Lvt zmhP>miXmDzkq116m^wQ$wzOIPDH1LFBYnEz!7$xA`n}-TKg3;flrRtZ>%vMX2~YXK z18aJT?Rwj$9~!>W$DtDkEzSHi`>{MpcnmbTpboMx;Ni$SxE7K>m~Z?PYH^b|ymbxG zGw?TG^((0@*Wnbqsr2qkJxz?uO31w#oJO}bT9*W_WYAev*Hkkvn8wx`4-n9_uTL~w zSWM?h=p2K&xnCqSOZk=WXHE_`3T;CC(8gwiF`|bGBhA%WI+RurSvGHxnIpGQJ)gZt zE-n)3S026Iu`#cz?^;HGIiS`fv+Ux8isH>3tvU=;%VUJ2zu{=(hDV3|pRlnH%jiye z3n6XGPKW~Dpho=PjypaxnKgy+TYHb-O!R^GXA4ahzg?V{${W}d3z<|krEXT4J3qZF z|8<5VQfUu~&HL|PS#o}vLS+^0O7#5hAPn>Rav|(&&W%}#0e(~RXsDo zPhtLfTiZ?947glQuLp>vt;BP zZ`{zJfEE+~6<2G@k?iR$$Yvrrvlq#+RXZqqlB*!!dys>JoisTdy%r_Fw62zFD(fj=-KvFKc%#$0q`Ak>xXLb&kOGW< z^+GMKL|y*PBC9tQkPKD`6p6g>don{oBFIJ1h<|rttyv(nfaeVb!?^m?UUwDiFOv%ndtH7ayELA1yv!w4>$ENN7}c%{C5}?E z@N##0(M0_T?dG7)EAO1VOMH)rqL` zUl08TF7*Ix+_Za-cRRO&rz-9~#m`ZJY#14NeZMyKHkeiI>+VA7ITsfc{t}MYn1V@K z;CvJ`OM0NLa3xs5Ot&WFr^)(0h{=GHcoe|Y>tV^{ga z(hZFU|L|VbB#q|zd4exh@BF_tEsmUT6}o?Q)Kr>VudPjyeX}He`s@VV9BCasg4PT2 zwawY7r>aL%PN#q?TgKw2X#F@Mt>MsPLGUDv(S2G6E_-OI26?RHa(|dwcW04<}{!aMZcOBS`H8dl0FKx^X zXY>GquUuO6sQ&o*M7X^6B=g_8SJA&F7J}#nkivL=5sD7hQ=!IVlhaN*a1o=_Q6AXK z88^EdBk9pgGn~y{WbEK?rCau+FUN3<*0j{RV>sDgP7iH2iF6Z zI5TZYC!RnkmH0TO58ge~y6e~TUC5A7&QHLz@aMA2U~1m0k{K%V zutc)Q5@^GGEZlMON|knKcU!yV7uyS3@5bu-KLipz_K`As! z110Z#Wb&aq55l?HA8CIhQd)j~6=8%ZnHvlVBSAi5C2lDVdYDWaRE2Ek#w3UDBmb7} zT^!={l3-A2J~5oP_?(XB<=h)~RH@Gh$9zp)1-v=oRxQ@|^RZC((ux+|kI5a0Si3_edu zv}aIWv8r*)piOFA`}+jMzNNd*8L%n*0`?>6HN+L;`X96(l=Xe<^uf7`C=s~TOulyV zBiDwrgtYZbp(l6C>^%X^yvn5A3d!4{Fhz2O=h68V{0##4LU*>Y0>`6WcS6~cQw%$uywH97T?@pb>#uJTj!|p8Op$|9SKR$=BMMn9jBL@dN8^8lHz0uTk@mHu z{HBj`dV)U1Q++iwul*bwY2q?9cOx(X@y}#qROac`5$=R8vz}8|*PTvV) zedNQti2mUDjXDy)BHkzd@s`7K>7aa)lJl>>wIeY$e0YtoICpdv`h?tq!ruF$ujzvd zQ-tmF?}b*;E6<~nNz$`j)A=S`RkT%sUCuRCtCM;=*4-fd8*a2R)>CUjSmi0!y!Vc6 z>d;7~VSK%*y-}k<_PL-^VinFnWGs}edmS#h&flB$Segl!lOzJF-y8QOtbQd;VlYTjRc;1hcvvFW%xY+%~nAnbmmU*7P_y_O8H_mpR{_yevEQ zVULVV!W$^$5Yw1J=GVgM^n_m)@dDR^wwZ+(egL|pUU}Ot$Zv<@RLN(nj@JaByF}w@ zMh8gD-8*S2-0ji)AKtFz$mi+j(uDi`w_KUgAnZ6!m<8Rku-C3D_0XdZyI1@uIM*sZtumW__~xWmRpq2K?xM4Ug1iK^u_hOgjp6emS>V= z6i2h->PrCMRnY0jW;jswg|k7Yl?C-n-G?17Ox}*-sH=2v#vTd6Zt(le1yy&+g4myN z((h>qPs?|97OuHdCZ}Fr2)OZD;d&`G>+2f0+@H1o;WU{w&D-8popP7rG@5kaNPPTt zabY!EGKsN41XsWNB72gba=RMx`y~L|L+hLn6~I=qD2w%{(UUE2@fiV}2J28@0Zkv!k9cl1dK)#K9g9 zEZn$B&*dZBEJ73}`N|w~5;o@@X1Re^6{=kSo)RVLLZlM_N^KS z&zgb!>c;Qky9YCbKJEqnpAF_m958VU@lJ>L2KIvnKv9;IUZSm_KkyvoEe|F}Y}gv? zG1hNQxzEI*cUF!;4DZ7afAUd{en6CsvC;}ZwZXjp@bjf-lSiU|d}A_M`hy<0Gqp!K zALXg?3%80XPip-50PI<;!~JM_0(S&1wX=}+e04tl#kRRawKC`rxwwXoLMbvk@n@cf zAm%L}q3TIL+Wo3UWCSg*uiS4Vj_KF0W540^I8Rc5BSVeS4rO&yEXR+`Io?dH|DX^f znkjh57g(&6y{omngZwisrl)xj`LP$m00$*N@mI}A=OOE>R@q)?x{2wl&TNZQ7WL{X z=77$bY+4$rP?5E*%W_~6F`DvrzwUWbFPB)3a;de|Ga?>)oe!BYKCN+viW-IQv5jXd;Tv()cjXhL*x0juUo7<0ns@0tv60LYc zcs#E*A zzB8gvF04O&@O0LgPc-MIgupK$s=~P2(uXpK_AK5mQ=i!(EUgsqC-q8EZqCfJ6gVg1 zFQY;M5LSXZu}NO3ah?%mlW~4fK{;v-l_{uB&u;gi2jd z_Me#RONXzdq|PUse(86@*B$2LEA?GwEz51O>(a)1?VbI8Tcs}YyVvfws`3pS`ni9q zJ6%tPX&4jQKg=E9=3qliQm-!ft-k;-4D`NM-Mdg{T3M^c@fj4xS?dMkJ z{rBg*#a%yz1{SLOxU@R<4PwC#{A z6iJb|K9k`;huE{ayr`q$zhLXwOdJ2+t0SSIVH`ea(@MUj*P{*7)Zv!4RjQxgTPhP- zer$L52w|x50dNT6OYVPo3md5xnhNwzzbLb)YCx<;C?ibx4=wCx!fz5rap@Y*iuLX# zvG$40)@G6|+ua{jWzDfMa>OBH#2Y&O$6JAG5T+xAbcm~2I|H|XjajCXe+cI^eA^j2 zJ~EWqk8d-5n!+c%-u|J6o-~W!z-)eoGxf{Dpe6APrcokjW!Ux<(2#+N*))=7J&yVt z>qAmeZp$82mwMTEx8S#Dg@T*|_4vIn==$=4F}#PYhXGe(x8t)69+YMFyc(oCX)?9p z5EP$GZK|(=kiPQ81?CZW0*GG>2+j0}FK2EYTh&c)Og>0b?SC^`Xm@(3p>l>ve2U6^ zIm@OuonE5|VgGhf(cEvCkren>%;BH=uI%P06{S+3#vmjqN4sWejRU%^{|} z2;{HrO2?E<+176EoP(S%2GW_$xi~0&nKe7WUzjs=qI}eCz}gW8v-eX0gleP(AHY<;SL)1;?7U z^7eLj!>Bex5G$ZVn&1C1m_w3gT7?y5#|v7_kYK>gqfN-7g^BFNR0G!EJ-p#8^&Ma6 zdkkUcM8SU_hGRhFcfEds=+@yqTj#rU3Q9EzCsSwN zVs{e-@mNm;pH%kqEUJD809QXtaP)g}=V-Y*g%efNT6lq?$;Bt_n});N6Cf$QleBh{ zb7jYnJ7$VK!yhitCRVUix>?IT27W;?Rs3h)Gj&URHEUasaTTg=M(T;)M)!D|jY!&j zEx3q{N}*%mR5g;;w3gI(Xxsxf{2NXG<~xzyQr`AH{*a^34}5^{RqrQ*pHVj*Jdhd+1 zmx~t4i}qLnLRsB)-XwWmkBQN5$cpkiHjHH%8ZTbmyM0#{Xae25OJ4%ir7jI0^4o1K zw064@ZkZCU>yNG}$6Q*Hw6Kn|P?9@LS1v}Mt~Fi*x?_SpxV!2zcoAfnOxe+0Q=Pa% z=9QVU8=I7)8^+@abg1N+M^q>ceWGmYiMhc9RvPDLDZWu~Us;Z5f(3uMCRs5tm-l0< zYQCIdn%e<4#CN1xWq(=XITrATvv_qY#kPChL+$So+M&y-dh8^Ay{?+4QdV3F7w0@F zC-SGYlh6xI(m_-(P3tE=3ShcV8m>_A%2E||Jf1*nd&D%74Hoj1i(WRV(IAXEb4(Y&Bz5eP_(dhAkc%`em!v<_9BcbK#VPBD#)o{qQ#x6WhA zkB<>II$*z0N)F;5G1oi(LYI0$bXUc-n}6!PZ?CebiSn(R5GXoZ5>;MKn~O5R=ggI# z3TYTsp$}D?IC9x!v@4rz1_qpo$YUzFS%wIHPDS`|N6c_JKB#HfD6`Rz8A@}vcD7jM zV~!(aePJn+7t@#b=)b%ubnc>R4a>4`N67w%^0V+iq@jwJxE+OQ#A)pA^!pr@SE`!JYD+gZfoe7TmNRd>{|I{Kt*OS0LPKvzGF8F%e5r8>u$ zjIupC$)w7*#%kB8@&cQ|o6vH8tXc~WG9$}`+bMsaz=`%c$F8fkryy$`qY=Ou#GlH> zN8~(`b!&f^-7x`ocPJmNxRG147wby>nuN@lFp~Mb^6XUnZZA6T`pG3(-`%UUK0l7u zseCP(lqv>`?JxI&%!XlY;8)ig%~KqwFusE3q{0+n(EO>0X zY;8#x)-&{+Rc&rOq7~+`G~=j}IOyr!mh^k3PSS*B|3jcgbBJ=YTin`^_}r%yge2`4 zW0@Z3gJ_{%jhyf+)>HHB$u<@&u7`1|FvRgjt!x>B+uqOBre(+&@G{T;Y-ZunaE^~8 z71DM;dbTgkm5H$Xj8)qm&x6nD3|ZIf+$7^VEIjzIOflOR|yTvyRk!i00pI#QTK zcUcPo)T^*XCe)?w9-A843CRiT4o6jTm)@_6BxZg?!=Rn2)~iOh!>N1s2OZp6=no#4 z*aKNiZbuwe%q<}7KW{y2tTqzI4b=_9d72;2?xZWQ2(b*5yn8v$$;N>S3^xrMr`HRR6=UJt#OXCt9a2;NXC;PYn<>H3`}7NuDM>RP+Zg z_!ne+7ZMx29{u_7vZez+Up;G$KOEJ<1qj+gOV{uRo4<&chsx;IXp*kHXRjw_@^-KA z4j0$^)Ql4;l5us{zwiGn!qpiwL{5zV7c-ka6Su?vsTS0JK=Tjp%D`3My>Sh6`2cHl z*NJmCvA*+T;%4?K#vY`<6ihNNzHudT8~ z6Q%h?wb{4+`8*!JQQV0V^q1goQSm8Og0hb$a4*sP`Ozn>p5?}Wq41S-DpvU;X__8- zaSNU&SD*u{FTUN6gMc(lp(X51)++0+>0_Idt5XMuk?|dLIu9)#;0cWqA`2#c)!kK8S5Z8N zs2_20q>JT|$Aa8rKq>UPZfo4?d6baKl;$HQnUnb<-*x<0l-+XRjlzl}1Gex3q5{b9 z)zP6sgTjbwz+?_ZX2+1){L61eN$MF3c9kRP%N_0ULZ*A=#92c$>t%xByVjbc5vQw) z2ttf$8W>#EH5*mP#it(EA2askoGdKW^Bd4{{m{?=sO+A<$K>@rPG3wR;qvVRkF_da z*<GjjoFC0Z^aYt4?DGgXu%K zY2qL1zW4@0Be#Xa&gWmAD;JL!l6TD#XLnjjdfz@S+bxyD1(pQ!8-}@rhxc2Jba1d2 z105Rv^u~MXhcvA7B5dk=P1eER>KdZYj7Q?|2`-;|16;3klBfw(BH>T@$v!TzN!W z%8$4=HymTD5Z`Y~Ow2naU|s%{*r6d1Z5o!3(ZeWikm0LAIQMPOF_n4 z2W_%y)8J7R6Z1PWrAxQBr+0Vs^x13FG!ylXbt_t>*Tcm7*^L|e;snDN)pH9UQ{}U~ z3iG?#k2X_DpP-~5|L`pI{Ex4D5H?JdX#cQ7cdo-<3Dq^h2X}|;=96Tfxc^RUjb1Ao zwAM}*)Sj@smamQ`@Ow9aA!r8CMT`Je&4PquUXjQcjS8tZ(&ANeKEBeM7;!b^huw&8 z%OMZlcp;9x^=&U(dDo)wRg>S*rd?}J9(+=e@wgbR`)UB8?xw)D<{*6E#tnr|v?X|p z;dxkyu1VaN)%)3JU%PnU+Q#9m(V^hc6NeDx^0~ zTqnG{|ET==bBFb8J{6Ql8}BhwVX>^Gzw4K=#oOXla0X9^EWz_!QAx67ZE220byf5?-LTJk?)btdWDFMpTfr!xU{8kAa>%}k z*$;A{^7?hF>6!S%*`!aulD4qrQ{Obj-E^|QK8 zQKI^9c*&xYH+L7&k(NK^1Q>8+Sc8_YDucv%nNFN2d8MCfsQer-gN3fKR@ksj>D@p^Z$G934M1 z(TU67dm^~}dSzp#I^DitdBe9gh0Qy^ZfYbt`p>6Wyf_UGMPhlik;zZGEuJ4H7VX0Q zJ)b_E|EXXa_`nG=xYd|dYr>nDU$o~z1dPjK=Lk9SU8ows+__yFy-8gEh$s^wsCy|r z4irL9I*3cX(NO9o5o)VO=cB$CbQUg@*gaJR4wj?zoRHqc00&4ZRVMW@rqo^v6XGPy zDC1;qWE5w(0QdFw_7mBfF=cM^xnw%&sdbU2QWQ7#pxNCuf_vxZPc7(*o)-H!vCO42 z-Wr{+Rl6bQWhP;blUU9UR|x687VfX#jt8L{@%jh!*!aT&7gNWnsXOp7ZYuA+RGIm{ zvt=tob;ruZPvda@m~!t<96h!CjmHf{f}5XC2smfU6ROBZqchn{ld}On?csB zCc=E<vC5hX-{D~$aY8tuufxytP84Cj$JIr*y_RYa#Mg;M7mWgsmIt|3(dP@{^B;&wlUeJ0b(svip$5KKbSRDSJc(kGD-{58W_-s|rj z;yq$CZ+aJDE5Qe9@Xu#@(lq5Ow3f_YHdvoIa-lfd6@((IqFls95QuBr8T*hk->nIN zs73_an>aE}DLEJK%tC@f+VS+dvY(6*l99Cf{{B~5nvn@fBOa8gp4OIVp^r5U6NHgK zRtjh^dZ6{CAm-$P=70n;87Ev}Fk8cvm!5hC3&~_oTm3U5YZy74Yg!xtCfNQ9SOTaS zI!5|Y7?a5VJ?h#+RzwuTuY`I8%{j63W8uk4TE5kn0)wu0(YP8aIK^n8_$B|dFV}B2 z)wcWBjg}a(l51+@FVYTjx(3xo+#ySbWTWNoeTZ(ehp{Saa%*H%cx3VEopwuid3yM3 z%c01NNEDrl%$+PPh=)9*3RZbP^?$MVonKA9U6&{#A_4-^1yq`J=^aF+3kXPWDhLFm z_ZpNYNRbkvv`DWZQbOoRZ_;~!0HH}wr~yKpJkK+;*1W%Wt(o})=2KQaB-eGH>rU>o z&pCVVw4wv5rl_ufl7$K8Fx6#f78s^*F}znI$_2?9`KMVy z@N!=1u?0bU(FvHI4TXX#EYE4SQkOc9%ujm}%@MBoxV|Z6GUtxD%45NK6`R3M+O|2CW{8y?tXG@9vzukX$Y7 zu=uQxI4UO}w;M)}qp`uzB=9B_#BFxSc)Df2IwFZI#?-5z$QgP+$CZ+^l&cJ}n+2uj z8cOji8R;qyeQ+g=SvXDM$y|L!UZfieX#cWLfYw4T*W2-ezkOhLaN7Em&n0a8xubi| zyP_vjYhk|ww~5@@V_;XNafIenBpb2iIJ3vN|5}{oX*;(+c?2#F_r-L^?WFwNZPQna zn_2MIZ1|fyWcTk7WlsjnyqA{?vFcbLU;br9kon2u&L<#e=g~5d+WK(i%z)M4TJX)x z3SE_`+~}K3($N!!#tw*E=7y59V)I#86ciBDQNY1)~ z{`JKn?n7tkx;DF%BdfG#=XwD}aKNhae3Jevf#UAEOFdAqaQmVsM=>4IiG#|S)tFyaxW^Q293l$nla%hp13puKky! zjj0Wvr16jMsj5={pS+xX+ci!L;y}|=?Bynr*^{X<+h}`5Wnp@SYauq@gH)<~+AdgY zVVBt8e@!AK#EEHCL-2T@*T2a;oEPLt|6(c45xR-Bm_N?rZ3o>4#4wKi<3c|e#Qldm zbO}BupDrQBKr!r`cmWRo_vfdb*kaL&9nRehqa_x+3j8fcz!l{Xk?J{O_Ck^N$csOF!=B7yuq&uLl3c z-TMD0_kTD!1lr|`OvH=2n$|C`82xxz9HO{ipxk&|ZGm2^Uh}gf4>>XSffju=nVO9)QLMuOgMImH zU?u$)cO`xeJ=n2CBAl~t3@aMT!`l3H(A8DG`@UxgHXaksIU|WcLX+@Z$PeCt&x*b2 z?u&9ZE5JfkG~<}t$>7nWrs(6o5Bcf4z1#u6Uhv6P$_sOF;?#M} zP6XfaG})MeE=!0ou(!Ms$CRg4w1}%Qpr0Q+b){Yt)Oa8(bUmq1O=Dlj@JGv)8Z(f^F_9Zsg?x&71*7n?lOva)ksgB2Lj87 zv{k4)Ob=1I6gFFh_$DxLe^quBvSopqsp0ZAI2oav!xh<9Ep9WG@=Q<75*4Z5gn>rY zuua`NXc_pWQ)6Aq=Ac>SJ8J3u-k>#N(p6Uc8ETGPf30c%u)UW>WgN1yMC&Z>GTrlz z+t}{m5f}SM_btQaoGr0ZsTze#+xYa1&Da;LuqesQuA{!A6s4qaeL0Sf#-vErgO4Gq ztKdo35EV~i9@(yh1@SF0-!A4OZ&Nf)aALg}3iXtnW~{J7WatiL>iH#KxzyuSLhWc` znnO#WNpr|~4W~h_oRA*Mk2#v@QogW{rjTDg?l^Uk&zG-nT7H}0qZ%XTjTWJc z*`NN%+j~0eQaGj?w2D*oQ_f!@XVP`l_|9m3XAe&Fqd8<_B{MyXyGz>dztQaLf57uC z1t?^qG@Mj|#6=i2O)82iQqpPnKk6BrcV0bWpejH{70183JQ|uq^`s=B2mO= z=?)Q>5ZK=V>z1zIAOFzNYGKYCx|iiRsFnb%DI#a#`QCWEW>V(qH*{CsP9@X%Q{bwxYs6#t=@mLWpA@q;v6cqxP>ge|X(Fn!ams}hKU62**7dP@fb)j} zYG!8(i%Rdj5>0pGvK1GxN?Fo#MkLf)Ca~9*8^|weoJytoPUW%){nmF=Fy{SM$5~E3 zj%Ui9>%E6*>FoDGlWy$oHjKPdy%H?OMFop|4&hn3w2X{%v1DHJxZkm%MZJq%Vn~2) zExBOpq6&I%%cQcyVf&kpO|u|iEL4;eMa&Gk6Lg85!fFLkreYPbb&{*>m!_akXQe&v zd)_(+L1A|;h|j-;UcFDqlqePab$*prtu_<0pSXnbY}b|S*2TAs8}q)b!%eUJl!9N^ zO?_2&t$14(>b!RbYVJ8a`p7c*lx515?;q>=6P23aPWXJo6eNkW9sbhQGYM zp6<=n+!kn0{xu(_l;GcJu$6n1wR_OELOJRyyDMC~2XKv5_6K=Ii+k_O{t-wo#*7%H zUK*ae5=ZaL>|*z3#o84Qn~xbj{T|(sIgBTRhv#L9lRUkpq!ILB8dlr#i1_ELbFW(S zFkOTEW4OE>syN-n!FY9wCCE0S@;LOB4gus)w>@6#Mb)QypJs%?du1e=2g{jsyf*17 zycxwUBcV@=Y&YHDCX%ie1<^Jz>QrBzd9_g1>aKEc4)3hgnJsJmC`F@!p_Mk;9iul*xPGVYsT}bd}!Xw)St8zB;{qMiMd0)S1*n+$@ zK6bxr;hcd7ix;HFMHK^uYgmjo$@G^6Ms5SqZD<0UHpT~%u%2E3TxbX zJAN;ayuyS6Z|^+7u4kX7v30S2FdFgckx(S<%PMK-K6P_Rm};eWZdCuc9vXMLSeVrW z5&g|<@bO4=a!Tts)M4Iq!si(DzAjpJS`B!)Rj*<@o|+)H{f+POpAl^!snbQL1#*kE ztmD=BxTO_gZ#Yo#Y8nkvRFdhuqU_3f+*;%0%L3kIOtGv)f9#vCDn@&<7KIh$H?Cw&%mHdzobw)uTYfIUv-Y1L%@$LS$A_csl??Vb3A&Ei2bZ3$iV zGeV_PmQ8UIgvP4kA{3M`3bqR^+t8e+<5yfuY^K??>4Xx-a%ixysJ5T5<2kqz@MRrQ z*nIyXvPdz?L1X{v8z89}C3=Y73f|l{qbU7zNHFmFrACMxd5Y&T=@Cp9XO7wVby4&M z29FeTW*wZ(GmGRiV6iHxnDX4T!!sU(v$!Ym;RVl7OqK-zRy+CL9}iUug6Moz zhdNI^d7S|_t36n&gl+^}orOz^(gJqKBj7$i+rC1(GJJP-zpjOpu^}6P^jkA1ris-F zMb+XVs^6^uI3IJNSmyPabzngIiRe!?B7BRZd9xHjNfpLuE*fFkcsK=y3d0nU6!kqkR z=O;d1Oj-M{*g&ZvF3!_{l1_#5RA+g+o9tnRzx;Y7vr>cp{3U@k10-vw?vKRdWh%BB zhRHhbGVZFRpd-^G#V)`OZjJ3%5kOb zbLH_ZsUZ!+Y|&=^{HHfe5EMs&`H`eb6pAy#l>xTTaDz&yrrho5UeoVK!ijmfJDb%q z$0hFBha)StGkpaOzKNSMSjv^2S+=fLjs{n|XAQjSU)jf>>Kl7rxeVzBL1JiPTd5Xp z7Pc9ydmYHrr&AI}7g8_mSTm=Y*`==U|q z2((|Oaq-LqXTGr!w;lEXck}F5P&*=%-hA+A-ny7yfFqK1gDZC9E4TCTt_Op<7Szbl z^o8U^jijQ)j}dgzUBHF$&);X>LOO|aN-j&jG_gTE(IsJQFICX#(PH*9&Pz`qiv1AX zsiaHA5MeA8A^{4|Y!Tao=GJAe1QHDG5BpvcTY)iH&UP5f)`4fJXOUE&KY&9AaHBaj zMNcrrje&0fwe-FDbv|2R!!aR~Y5*sI>V4&@gh~~ivv}K=!D!XXSO4G|&a_*;<@*HO za$bGs#sN{-D7=buytNd^uu6_K4L=bUx%5A+1F`12uVg&N3{NKZ1$f&SQbKc7$Gwv*>_rPz}*0#o(Cv(q!<-x5O&%;zM2CFbVL2H}BW+jOR90 zN00u+$l)i6{{C-9PKG^E?lSu8fRD-MN1u6D$6gNhi36@9sK^PMOJM$jsh4C~h4?M5 z2RW)k7DP9L8Y0rp6?{&F+e93L2N|c?Ytq6^` z{m~{0ogZBNB_XO}5P>Fz0}~|AJ3)t>GCfLK|7y7HzY`igwhaKUm`+xV~$~!;Wo}g8W)vwA}9aQ_DC*6 zkf?GfL%ZFozX>i+i_B;j-C}_piz7DfgNZD@5d<-C ze@VXo9#yT&Vji9Tfju(dtRgaKX>-uLN(WWKC?p=#}j zbbk9`y^aCz+52xUO!f0|OuF@m+HdBI&P5mguj7WT0%3{<$F{M5(tVW&kbVXkk!D z*u`mp{6dwc2NnD=BX?!wqq0ivOzn9Q29EdCkgpLl2)}kQ_SyN~a=~6czFP;CmNB{N zj#uGEAgB>_361KCI{VINv5InDZhOfb*XhO?dNWk&X-=Skuxt3|04t*TBY(;LlrZ80 zmURqE^Q(<>(fJ`71Hb$c&C_Qvss7{agrRqj%lCjQ%-oL~1Br1Xj%1Q<2Ys6FR642K zKu`}1RE*Fu^;W0jcN#{I>`==pozPQYu33O8vO~XA)NZ+D12$pt90RQyrW+mhb8K z9^*_0!kg(RzaEkPr0-AntveFi4S2y?%;v}DP|sx*4vmrkqPy6|_BayRGxqB7Y^7I; z#4MdNpCL8lh9x0C{{2ue&_r>2U|_>G=5j)p5y7}8sgTDh5plPlZ!*gR_0(2-R-67-ju7XrQDqvuR&7Ie!Kl=W zn@D4;oQhZB@)y%7rIadxbonB`*YFbF6&j@qa*fx7H{8GL8?e2DthBfrGo`XTf1(4( z-2Ci2>xpa}qF)j1Y6`5(uqJ=~%C*pVG107)#JBiQ+wCyk`Ib4b>Ll2=HZBts_)eA` zAlGSaiZYCE5=TUw0FDo??Ocr3B7$)niHoe7*N~BP0cW!K@_GmO7@mB!slK-Fx>b8& z&3qo{&UjhhU~hv;_B)(uyUM*<1$GM=%+%C?E~_h|A=cbFBGEJF2fejP=8FF3nh+7J zKJwTy7iP7DY-v_DjDFmr9luKX@wl|qkji4mcan>RFW6sJ*U_+gTA5jCx6!w@No4X| z`ulTdSu2?h@>Q!){Kd=i+>Yr~-Z#vYe7fUOK_;*j{WvB9d-PPuqcO$9rbQv?zE%fm zknl*Q{>p1!+*|CmgW8l3F2UaW1{BP{+W3bxcaXKd5AlNsJkPA#NR!Qd0u}WuUQ&(A z7xwdgA|KG;%(lQtrvgiUowiapCo5`sizRliJ$jOIF8&tb!@LOTXmnC$#vSb;YuY|R zJ|t_?HYXpY1h5BBFNGInF1sn3ueRM;1hA^5K?%uJt*07qN{UI&25(~}%8rWD-|;+C zJX!7vFAF>*qbKcC}tAOzuJq~uc^)--4Ro~#(;Op1$g1JOxc(f>6Ko1Sw7V2(8 z<2bAr`J?eYQd@b$P(5HdX}Hu0NFCyyYo<7jB5%}z)$$)6bw1QE=^NpqO6qB>J{Jg( z7t1hk7Hc%oQgBY$a_i_S7C3U=v)-vJwp+d>B8pdAhN=g;+MrcxDJ-6le=EP;n0zb( z6tnuRE!9?KVXjpiTIb`CPL8-^|FuaXnV^Bu1Ctykn^oE4gecOH8-rK&aWR_< zp?$Kam3$UB1N8Zc<@ZOUaCB6wiFG7x;=(i(79L_(fE!R!^J%eBFxxcZPEn6m5e=W) zELzz3nXRo&tqV=8km8?&-3BOG*ajSu6 zccRKVF~!}b`BrLIG5#Bhf7;boeo-A9=ufsX1k{aj=+x}8!+D>CdJmabW7MIAc zU&_}eNHj_O>I)#Dh9m?HO@GJIS9N>%#1K=K_+lfNXiC*a8gi%Wf- z*>~nFFHZZqj>1W)gJ<#{VEdbNZ3WG$Y>oZF7r25FtoGpU#yEU+I65C`L!ylAuOLXcgo23JjVSV&+ntL0nVB)@ut7A$^x(Y zu@c2njCn{et-=}NS@K?T%?$AKCFT_}PEF6~Uz|fE7XSoFj9)ogrgr><0im@hoHI5k zSo(FP?)WnQq_7&7>?h>@e&$$J?rGIQLycIUKS$X6)0?djqJF=nX7iYcqW+4@P_P!@ zge>k%Uk9eBzFRqf%_6N``ADAI>%6?zOa)W3f5*Cb{M`dM4rSC6o57ChE9 zk&=;SQntP4bY{A!G%nuz*7+^gm|siE;do*l$DzZWLx$yMb&bd9yygB&(rh*r8Ao~F zJ15xGu@LvzsV->VNxn30f8&Xv7V}3M-EZXKVp&&eetf@V=xv`l#^^WNt0qbR67-P& zB=7wUFHdW>s4A6^N10uQma~6ZB6qU-Rd6=J2`V+S5xAcjlyzE`QTGLNL1X|kiqhHq zCb)JW1xmj<-d^y1K6%G3{|`}!b{p!;XAA2FR@ zNFIsJ3zxpkp5C>SQwrO&1h-!n0JAhJuBYrA*-a?9SeO+Qwj|T%A(h>6?qyo81??;D zbWUeur6CM6BhVxkWRUq?3IN(eZpOFG{)$2epAreLx=Gh4FQ)oSv& zY5mZ0iCg~N-&I3yn!Iv1caL&T)Hhdl#hm@+qgr;5y2}2!u6=VPUNo0&rBItkbe>ul z-^&cO>XNd!a9lbRN-dahI*4H4xX0Dnw>8LKuh%d2>w1cF>|%W1bx|`q=$WEKFBcX zNmUxi^8Co|^4IcAT!`x{2)(bz&6~3{gr|uuTpexK@jA=499tPt@RNk0NSWI_AUG31E_R7N>cl(!~#0+Rs_dmoVm;-RH?-O zMU>H(>{nh5rFxjDatF9p7@$MwI0atF31(D+SCa5)Fbd$T%?OcV%HwTpNVpSfyH3yi zAKJ`U12u+^qeNQ_l6%Hie4Q6XZCn~f07S+Ym~rjLbuP>+7h3{DOnloP!xB}TY&}X`iihl;!M{la|EE`W(J#O?vgNkR8(Ks@!I|%nsSbMS{91r0p`8x66}sqc z+~OV^q;aUBMDy`t@Gpt>`|b3F-p$@kH!(N1)6iZ2-WD}&e%9=S-Z-C<10RweW#sBD6>y|OTT!tRsc>-vUxtMfytiR-hEU;V%{Lk1cIESBb9j?8t—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 { + 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..56435a9c9 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartAxes.ts @@ -0,0 +1,840 @@ +// ── 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, +} 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 ──────────────────────────────────────────────────────────── + +export class StaticSmithResistanceTickProvider { + private readonly _major = [0.2, 0.5, 1, 2, 5, 10, 20, 50]; + private readonly _minor: number[] = [ + /* tier 1 – clip at X=5 */ + 0.1, 5, 0.3, 5, 0.4, 5, 0.6, 5, 0.7, 5, 0.8, 5, 0.9, 5, 1.2, 5, 1.4, 5, 1.6, 5, 1.8, 5, 3, 5, 4, 5, + /* tier 2 – clip at X=2 */ + 0.05, 2, 0.15, 2, 0.25, 2, 0.35, 2, 0.45, 2, 0.55, 2, 0.65, 2, 0.75, 2, 0.85, 2, 0.95, 2, 1.1, 2, 1.3, 2, 1.5, + 2, 1.7, 2, 1.9, 2, 2.5, 2 /* tier 3 – clip at X=1 */, 0.025, 1, 0.075, 1, 0.125, 1, 0.175, 1, 0.225, 1, 0.275, + 1, 0.325, 1, 0.375, 1, 0.425, 1, 0.475, 1, 0.525, 1, 0.575, 1, 0.625, 1, 0.675, 1, 0.725, 1, 0.775, 1, 0.825, 1, + 0.875, 1, 0.925, 1, 0.975, 1, 1.25, 1, + ]; + constructor(_opts: any) {} + getMajorTicks(_a: number, _b: number, _r: any): number[] { + return [...this._major]; + } + getMinorTicks(_a: number, _b: number, _r: any): number[] { + return [...this._minor]; + } + attachedToAxis(_axis: any): void {} + detachedFromAxis(): void {} +} + +export class StaticSmithReactanceTickProvider { + private readonly _major = [0.2, 0.5, 1, 2, 5, 10, 20, 50]; + private readonly _minor: number[] = [ + /* tier 1 – clip at R=5 */ + 0.1, 5, 0.3, 5, 0.4, 5, 0.6, 5, 0.7, 5, 0.8, 5, 0.9, 5, 1.2, 5, 1.4, 5, 1.6, 5, 1.8, 5, 3, 5, 4, 5, + /* tier 2 – clip at R=2 */ + 0.05, 2, 0.15, 2, 0.25, 2, 0.35, 2, 0.45, 2, 0.55, 2, 0.65, 2, 0.75, 2, 0.85, 2, 0.95, 2, 1.1, 2, 1.3, 2, 1.5, + 2, 1.7, 2, 1.9, 2, 2.5, 2 /* tier 3 – clip at R=1 */, 0.025, 1, 0.075, 1, 0.125, 1, 0.175, 1, 0.225, 1, 0.275, + 1, 0.325, 1, 0.375, 1, 0.425, 1, 0.475, 1, 0.525, 1, 0.575, 1, 0.625, 1, 0.675, 1, 0.725, 1, 0.775, 1, 0.825, 1, + 0.875, 1, 0.925, 1, 0.975, 1, 1.25, 1, + ]; + constructor(_opts: any) {} + getMajorTicks(_a: number, _b: number, _r: any): number[] { + return [...this._major]; + } + getMinorTicks(_a: number, _b: number, _r: any): number[] { + return [...this._minor]; + } + attachedToAxis(_axis: any): void {} + detachedFromAxis(): void {} +} + +/** + * 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?.gridCalculator) return null; + // parentSurface is on AxisBase2D, not AxisCore (TickProvider's parentAxis type) — cast required + const surface = (rAxis as any).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; + if (!yAxis) return null; + // parentSurface is on AxisBase2D, not AxisCore (TickProvider's parentAxis type) — cast required + const surface = (yAxis as any).parentSurface; + if (!surface) return null; + // Z resistance axis is always at xAxes[0] per registration order in drawExample.ts + 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: any, + _majorTickLabels: string[], + _ticksSize: number, + _labelProvider: any, + _drawLabels: boolean, + _drawTicks: boolean, + _labelInfos?: any[] + ): void { + (this as any).desiredHeight = 0; + (this as any).desiredWidth = 0; + (this as any).desiredTicksSize = 0; + } + + public override drawLabels( + renderContext: WebGlRenderContext2D, + _axisAlignment: any, + _isInnerAxis: any, + _tickLabels: any, + _tickCoords: any, + _axisOffset: any, + labelStyle: any, + _isVerticalChart: any, + _isFlippedCoordinates: any, + labelProvider: any, + _labelInfos?: any + ): void { + const axis = (this as any).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 as any).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 as any; + const majorTicks: number[] = tp?.getMajorTicks(0, 0, null) ?? []; + + 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 as any).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 as any).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 as any).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 as any).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 as any).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 as any).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 as any; + + 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)); + 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, null) as number[]) { + drawArc(r, 0); // full circles + } + } else { + // Minor ticks are encoded as flat pairs [r, xClip, r, xClip, ...] + const minor = tp.getMinorTicks(0, 0, null) as number[]; + 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 as any; + + 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)); + 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, null) as number[]) { + drawArc(x, 0); // full arcs + } + } else { + // Minor ticks are encoded as flat pairs [x, rClip, x, rClip, ...] + const minor = tp.getMinorTicks(0, 0, null) as number[]; + 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..baab254f1 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartChain.ts @@ -0,0 +1,150 @@ +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); + step.arcPoints.forEach((p) => ds.append(p.re, p.im)); + 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..790d77e38 --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartGridCalculator.ts @@ -0,0 +1,444 @@ +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) + clipOffset: number; // the k in clipMajors[nClip - tier - k]; larger = shorter clips + useCompactRange: boolean; // restrict lower R/G boundary to circles centred in viewport + maxTiers: number; // 0 = auto (nClip − clipOffset); >0 hard cap on tier iterations + minGapPx: number; // min pixel-radius gap between boundaries to allow subdivision + _version: number; // incremented on every change; drives stale-check invalidation +} + +export const smithGridConfig: SmithGridConfig = { + majorPxThreshold: 15, + minorPxThreshold: 5, + targetTicks: 8, + clipOffset: 1, + useCompactRange: true, + maxTiers: 0, + minGapPx: 5, + _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[], + clipMajorsSorted: number[], + pixPerUnit: number, + xRange: NumberRange, + yRange: NumberRange, + isAdmittance: boolean, + isXFamily: boolean +): number[] { + const result: number[] = []; + const nClip = clipMajorsSorted.length; + if (nClip === 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)]; + + const autoMaxTiers = Math.max(1, nClip - cfg.clipOffset); + const maxTiers = cfg.maxTiers > 0 ? Math.min(cfg.maxTiers, autoMaxTiers) : autoMaxTiers; + + for (let tier = 1; tier <= maxTiers; tier++) { + const clipIdx = Math.max(0, nClip - tier - cfg.clipOffset); + const clip = clipMajorsSorted[clipIdx]; + + const tierValues: number[] = []; + + for (let i = 0; i + 1 < levelBoundaries.length; i++) { + const lo = levelBoundaries[i]; + const hi = levelBoundaries[i + 1]; + + const pixGap = pixelRadius(lo, pixPerUnit) - pixelRadius(hi, pixPerUnit); + if (pixGap < cfg.minGapPx) 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; + + 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, + xMajorSorted, + pixPerUnit, + xRange, + yRange, + isAdmittance, + false + ); + this._xMinor = computeTieredMinors( + candidates, + xMajorSorted, + rMajorSorted, + pixPerUnit, + xRange, + yRange, + isAdmittance, + true + ); + } +} 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..daaf1c98d --- /dev/null +++ b/Examples/src/components/Examples/FeaturedApps/ScientificCharts/SmithChart/smithChartMarkers.ts @@ -0,0 +1,566 @@ +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; + for (let i = 0; i <= n; i++) { + const angle = (i / n) * 2 * Math.PI; + ds.append(cx + rad * Math.cos(angle), rad * Math.sin(angle)); + } +} + +export function populateXArc(ds: XyDataSeries, xVal: number) { + ds.clear(); + if (!isFinite(xVal)) return; + + if (Math.abs(xVal) < 0.001) { + ds.append(-1, 0); + ds.append(1, 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; + for (let i = 0; i <= n; i++) { + const angle = startAngle + (i / n) * (endAngle - startAngle); + ds.append(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)); + } +} + +export function populateCircle(ds: XyDataSeries, cx: number, cy: number, radius: number) { + ds.clear(); + if (radius < 0.001) return; + const n = 200; + for (let i = 0; i <= n; i++) { + const angle = (i / n) * 2 * Math.PI; + ds.append(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)); + } +} + +// ── 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 : (

-