From 56bb51d1c8cf0c389dda47f4a96f45dd9e2ef138 Mon Sep 17 00:00:00 2001 From: Igor Cuckovic Date: Sun, 22 Feb 2026 20:36:48 +0100 Subject: [PATCH 1/2] SmithChart --- .../src/components/AppRouter/examplePaths.ts | 7 +- Examples/src/components/AppRouter/examples.ts | 1 + .../ModifyAxisBehavior/SmithChart/README.md | 68 ++ .../ModifyAxisBehavior/SmithChart/angular.ts | 15 + .../SmithChart/drawExample.ts | 634 ++++++++++++++++++ .../SmithChart/exampleInfo.tsx | 66 ++ .../ModifyAxisBehavior/SmithChart/index.tsx | 11 + .../SmithChart/smith-chart.jpg | Bin 0 -> 43248 bytes .../ModifyAxisBehavior/SmithChart/vanilla.ts | 19 + 9 files changed, 818 insertions(+), 3 deletions(-) create mode 100644 Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/README.md create mode 100644 Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/angular.ts create mode 100644 Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/drawExample.ts create mode 100644 Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/exampleInfo.tsx create mode 100644 Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/index.tsx create mode 100644 Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/smith-chart.jpg create mode 100644 Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/vanilla.ts diff --git a/Examples/src/components/AppRouter/examplePaths.ts b/Examples/src/components/AppRouter/examplePaths.ts index 52ef139ff..95f7aa0d1 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", @@ -60,13 +59,14 @@ 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", "../Examples/Charts2D/ModifyAxisBehavior/SecondaryYAxes", + "../Examples/Charts2D/ModifyAxisBehavior/SmithChart", "../Examples/Charts2D/ModifyAxisBehavior/StaticAxis", "../Examples/Charts2D/ModifyAxisBehavior/VerticalCharts", "../Examples/Charts2D/ModifyAxisBehavior/VerticallyStackedAxes", @@ -128,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", @@ -168,5 +169,5 @@ export default [ "../Examples/FeaturedApps/ShowCases/OilAndGasDashboard", "../Examples/FeaturedApps/ShowCases/PopulationPyramid", "../Examples/FeaturedApps/ShowCases/ServerTrafficDashboard", - "../Examples/FeaturedApps/ShowCases/WebsocketBigData", + "../Examples/FeaturedApps/ShowCases/WebsocketBigData" ]; diff --git a/Examples/src/components/AppRouter/examples.ts b/Examples/src/components/AppRouter/examples.ts index f4c6daa11..312a77e9a 100644 --- a/Examples/src/components/AppRouter/examples.ts +++ b/Examples/src/components/AppRouter/examples.ts @@ -193,6 +193,7 @@ export const MENU_ITEMS_2D: TMenuItem[] = [ EXAMPLES_PAGES.chart2D_modifyAxisBehavior_SecondaryYAxes, EXAMPLES_PAGES.chart2D_modifyAxisBehavior_VerticalCharts, EXAMPLES_PAGES.chart2D_modifyAxisBehavior_CentralAxes, + EXAMPLES_PAGES.chart2D_modifyAxisBehavior_SmithChart, EXAMPLES_PAGES.chart2D_modifyAxisBehavior_StaticAxis, EXAMPLES_PAGES.chart2D_modifyAxisBehavior_VerticallyStackedAxes, EXAMPLES_PAGES.chart2D_modifyAxisBehavior_LogarithmicAxis, diff --git a/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/README.md b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/README.md new file mode 100644 index 000000000..c02b6aea5 --- /dev/null +++ b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/README.md @@ -0,0 +1,68 @@ +# Central Axes (Central Axes Layout Example) + +## Overview + +This example demonstrates how to customize the axes layout in SciChart.js by placing the axes in the center of the chart. The example creates a chart with central axes that cross at the data value (0,0) (oscilloscope style) and includes implementations for React, Vanilla JavaScript/TypeScript, and Angular. + +## Technologies Used + +- SciChart.js (core charting library) +- SciChart Angular Component +- SciChart React Component +- TypeScript and JavaScript + +## Code Explanation + +The heart of the example is the `drawExample` function (provided in both JavaScript and TypeScript versions). This function creates a SciChart Surface and configures a custom layout manager (CentralAxesLayoutManager) to display the x- and y-axes centrally. Key points include: + +- **Chart Creation:** A SciChart Surface is created with a specified theme. +- **Custom Axis Layout:** The `CentralAxesLayoutManager` is instantiated with options to position the horizontal and vertical axes at data value 0, ensuring that the axes pan with the chart. +- **Axis Configuration:** The x-axis and y-axis are configured as inner axes by setting `isInnerAxis` to true. This positions them in the center of the chart. Additional styling (like label color and axis borders) is applied. +- **Data Series and Annotation:** A fast line renderable series is added that draws a butterfly curve generated by a helper function. Furthermore, a text annotation is added to describe the chart’s functionality. +- **Interaction Modifiers:** Standard chart interaction modifiers (zoom pan, mouse wheel zoom, and zoom extents) are included to allow users to interact with the chart. + +The example also includes framework-specific files: + +- **Angular:** The `angular.ts` file sets up a standalone Angular component that uses the SciChart Angular component and binds its `drawExample` function. +- **React:** The `index.tsx` file presents a React component which uses the SciChart React wrapper to initialize the chart with the `drawExample` function. +- **Vanilla:** The `vanilla.js` and `vanilla.ts` files demonstrate how to create and dispose of the chart in a plain JavaScript/TypeScript environment. + +## Customization + +Key configuration options in this example include: + +- **Axis Positioning:** You can modify the options passed to the `CentralAxesLayoutManager` to change the axis alignment. By default, the axes are set to cross at (0,0) using the data value coordinate mode, but commented code shows how to use relative coordinate modes as well. +- **Animation:** The fast line renderable series uses a fade animation with a duration of 500ms, which can be adjusted as needed. +- **Styling:** Colors for axis labels, axis borders, and the renderable series stroke are set based on a theme. These can be customized to match your desired appearance. + +## Running the Example + +To run any example from the SciChart.JS.Examples repository, follow these steps: + +1. **Clone the Repository**: Download the entire repository to your local machine using Git: + + ```bash + git clone https://github.com/ABTSoftware/SciChart.JS.Examples.git + ``` + +2. **Navigate to the Examples Directory**: Change into the `Examples` folder: + + ```bash + cd SciChart.JS.Examples/Examples + ``` + +3. **Install Dependencies**: Install the necessary packages using npm: + + ```bash + npm install + ``` + +4. **Run the Development Server**: Start the development server to view and interact with the examples: + + ```bash + npm run dev + ``` + + This will launch the demo application, allowing you to explore various examples, including the one in question. + +For more detailed instructions, refer to the [SciChart.JS.Examples README](https://github.com/ABTSoftware/SciChart.JS.Examples/blob/master/README.md). diff --git a/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/angular.ts b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/angular.ts new file mode 100644 index 000000000..1e5fa5e21 --- /dev/null +++ b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/angular.ts @@ -0,0 +1,15 @@ +import { Component } from "@angular/core"; +import { ScichartAngularComponent } from "scichart-angular"; +import { drawExample } from "./drawExample"; + +@Component({ + standalone: true, + selector: "app-root", + imports: [ScichartAngularComponent], + template: ``, +}) +export class AppComponent { + title = "scichart-angular-app"; + + drawExample = drawExample; +} diff --git a/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/drawExample.ts b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/drawExample.ts new file mode 100644 index 000000000..cd5c22779 --- /dev/null +++ b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/drawExample.ts @@ -0,0 +1,634 @@ +import { appTheme } from "../../../theme"; +import { + ChartModifierBase2D, + EAxisAlignment, + ECoordinateMode, + EHorizontalAnchorPoint, + EllipsePointMarker, + EVerticalAnchorPoint, + FastLineRenderableSeries, + ModifierMouseArgs, + MouseWheelZoomModifier, + NumericAxis, + NumberRange, + PinchZoomModifier, + SciChartSurface, + TextAnnotation, + TSciChart, + XyDataSeries, + XyScatterRenderableSeries, + ZoomExtentsModifier, +} from "scichart"; + +export const drawExample = async (rootElement: string | HTMLDivElement) => { + const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, { + theme: appTheme.SciChartJsTheme, + }); + + // Configure axes for Smith Chart (-1.15 to 1.15 range to show the full chart with padding) + sciChartSurface.xAxes.add( + new NumericAxis(wasmContext, { + visibleRange: new NumberRange(-1.15, 1.15), + axisAlignment: EAxisAlignment.Bottom, + drawMajorGridLines: false, + drawMinorGridLines: false, + drawMajorBands: false, + drawLabels: false, + drawMajorTickLines: false, + drawMinorTickLines: false, + }) + ); + + sciChartSurface.yAxes.add( + new NumericAxis(wasmContext, { + visibleRange: new NumberRange(-1.15, 1.15), + axisAlignment: EAxisAlignment.Left, + drawMajorGridLines: false, + drawMinorGridLines: false, + drawMajorBands: false, + drawLabels: false, + drawMajorTickLines: false, + drawMinorTickLines: false, + }) + ); + + // Define colors for grid lines + const gridColor = appTheme.ForegroundColor + "30"; // Light gray with transparency + const outerCircleColor = appTheme.ForegroundColor + "80"; // Darker for outer circle + + // Outer circle (unit circle) + sciChartSurface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: createCircle(wasmContext, 0, 0, 1, 360), + stroke: outerCircleColor, + strokeThickness: 2, + }) + ); + + // Constant resistance circles (r values) + const resistanceValues = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 3.0, 4.0, 5.0, 10, 20, 50]; + const majorResistanceValues = [0.2, 0.5, 1.0, 2.0, 5.0, 10, 20, 50]; + + resistanceValues.forEach((r) => { + const center = r / (1 + r); + const radius = 1 / (1 + r); + const isMajor = majorResistanceValues.includes(r); + + sciChartSurface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: createCircle(wasmContext, center, 0, radius, 360), + stroke: gridColor, + strokeThickness: isMajor ? 1 : 0.5, + }) + ); + + // Add resistance labels on the horizontal axis + if (isMajor) { + const labelX = center - radius; + sciChartSurface.annotations.add( + new TextAnnotation({ + text: r >= 1 ? r.toString() : r.toFixed(1), + x1: labelX, + y1: 0, + xCoordShift: 0, + yCoordShift: -15, + fontSize: 10, + textColor: appTheme.ForegroundColor, + opacity: 0.7, + horizontalAnchorPoint: EHorizontalAnchorPoint.Center, + }) + ); + } + }); + + // Constant reactance arcs (x values) + const reactanceValues = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 3.0, 4.0, 5.0, 10, 20, 50]; + const majorReactanceValues = [0.2, 0.5, 1.0, 2.0, 5.0, 10, 20, 50]; + + // Positive reactance (upper half) + reactanceValues.forEach((x) => { + const isMajor = majorReactanceValues.includes(x); + + sciChartSurface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: createReactanceArc(wasmContext, x, true), + stroke: gridColor, + strokeThickness: isMajor ? 1 : 0.5, + }) + ); + + // Add reactance labels on the outer circle where the arc meets it + if (isMajor) { + const xVal2 = x * x; + const labelX = (xVal2 - 1) / (1 + xVal2); + const labelY = 2 * x / (1 + xVal2); + + sciChartSurface.annotations.add( + new TextAnnotation({ + text: `+${x >= 1 ? x.toString() : x.toFixed(1)}`, + x1: labelX, + y1: labelY, + xCoordShift: 15, + yCoordShift: 0, + fontSize: 10, + textColor: appTheme.ForegroundColor, + opacity: 0.7, + horizontalAnchorPoint: EHorizontalAnchorPoint.Left, + verticalAnchorPoint: EVerticalAnchorPoint.Center, + }) + ); + } + }); + + // Negative reactance (lower half) + reactanceValues.forEach((x) => { + const isMajor = majorReactanceValues.includes(x); + + sciChartSurface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: createReactanceArc(wasmContext, x, false), + stroke: gridColor, + strokeThickness: isMajor ? 1 : 0.5, + }) + ); + + // Add reactance labels on the outer circle where the arc meets it + if (isMajor) { + const xVal2 = x * x; + const labelX = (xVal2 - 1) / (1 + xVal2); + const labelY = -2 * x / (1 + xVal2); + + sciChartSurface.annotations.add( + new TextAnnotation({ + text: `-${x >= 1 ? x.toString() : x.toFixed(1)}`, + x1: labelX, + y1: labelY, + xCoordShift: 15, + yCoordShift: 0, + fontSize: 10, + textColor: appTheme.ForegroundColor, + opacity: 0.7, + horizontalAnchorPoint: EHorizontalAnchorPoint.Left, + verticalAnchorPoint: EVerticalAnchorPoint.Center, + }) + ); + } + }); + + // Add horizontal axis line (real axis) + sciChartSurface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: createLine(wasmContext, -1, 0, 1, 0), + stroke: gridColor, + strokeThickness: 1, + }) + ); + + // Add axis label at the left end + sciChartSurface.annotations.add( + new TextAnnotation({ + text: "0.0", + x1: -1, + y1: 0, + xCoordShift: -20, + yCoordShift: 0, + fontSize: 10, + textColor: appTheme.ForegroundColor, + opacity: 0.7, + horizontalAnchorPoint: EHorizontalAnchorPoint.Right, + verticalAnchorPoint: EVerticalAnchorPoint.Center, + }) + ); + + // ═══════════════════════════════════════════════════════ + // INTERACTIVE ELEMENTS - Draggable point with highlights + // ═══════════════════════════════════════════════════════ + + const rHighlightColor = "#FF4444"; // Red for constant resistance + const xHighlightColor = "#4488FF"; // Blue for constant reactance + const gammaColor = "#44CC44"; // Green for |Γ| and point + + // Highlighted constant-R circle (red) + const rCircleDS = new XyDataSeries(wasmContext); + sciChartSurface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: rCircleDS, + stroke: rHighlightColor, + strokeThickness: 2.5, + }) + ); + + // Highlighted constant-X arc (blue) + const xArcDS = new XyDataSeries(wasmContext); + sciChartSurface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: xArcDS, + stroke: xHighlightColor, + strokeThickness: 2.5, + }) + ); + + // |Γ| circle (green, dashed) + const gammaCircleDS = new XyDataSeries(wasmContext); + sciChartSurface.renderableSeries.add( + new FastLineRenderableSeries(wasmContext, { + dataSeries: gammaCircleDS, + stroke: gammaColor, + strokeThickness: 1.5, + strokeDashArray: [5, 5], + }) + ); + + // Draggable point marker (green) + const pointDS = new XyDataSeries(wasmContext); + pointDS.append(0, 0); + sciChartSurface.renderableSeries.add( + new XyScatterRenderableSeries(wasmContext, { + dataSeries: pointDS, + pointMarker: new EllipsePointMarker(wasmContext, { + width: 14, + height: 14, + fill: gammaColor, + stroke: "#FFFFFF", + strokeThickness: 2, + }), + }) + ); + + // ═══════════════════════════════════════════════════════ + // TEXT READOUTS - Γ, |Γ|, Z displayed on chart + // ═══════════════════════════════════════════════════════ + + const gammaTextAnnotation = new TextAnnotation({ + x1: 0.02, + y1: 0.02, + xCoordinateMode: ECoordinateMode.Relative, + yCoordinateMode: ECoordinateMode.Relative, + text: "Γ = 0.000 + j0.000", + fontSize: 14, + fontFamily: "monospace", + textColor: appTheme.ForegroundColor, + horizontalAnchorPoint: EHorizontalAnchorPoint.Left, + verticalAnchorPoint: EVerticalAnchorPoint.Top, + }); + + const gammaMagTextAnnotation = new TextAnnotation({ + x1: 0.02, + y1: 0.06, + xCoordinateMode: ECoordinateMode.Relative, + yCoordinateMode: ECoordinateMode.Relative, + text: "|Γ| = 0.000", + fontSize: 14, + fontFamily: "monospace", + textColor: gammaColor, + horizontalAnchorPoint: EHorizontalAnchorPoint.Left, + verticalAnchorPoint: EVerticalAnchorPoint.Top, + }); + + const impedanceTextAnnotation = new TextAnnotation({ + x1: 0.02, + y1: 0.10, + xCoordinateMode: ECoordinateMode.Relative, + yCoordinateMode: ECoordinateMode.Relative, + text: "Z = 1.000 + j0.000", + fontSize: 14, + fontFamily: "monospace", + textColor: appTheme.ForegroundColor, + horizontalAnchorPoint: EHorizontalAnchorPoint.Left, + verticalAnchorPoint: EVerticalAnchorPoint.Top, + }); + + const resistanceLabel = new TextAnnotation({ + x1: 0.02, + y1: 0.14, + xCoordinateMode: ECoordinateMode.Relative, + yCoordinateMode: ECoordinateMode.Relative, + text: "R = 1.000", + fontSize: 14, + fontFamily: "monospace", + textColor: rHighlightColor, + horizontalAnchorPoint: EHorizontalAnchorPoint.Left, + verticalAnchorPoint: EVerticalAnchorPoint.Top, + }); + + const reactanceLabel = new TextAnnotation({ + x1: 0.02, + y1: 0.18, + xCoordinateMode: ECoordinateMode.Relative, + yCoordinateMode: ECoordinateMode.Relative, + text: "X = 0.000", + fontSize: 14, + fontFamily: "monospace", + textColor: xHighlightColor, + horizontalAnchorPoint: EHorizontalAnchorPoint.Left, + verticalAnchorPoint: EVerticalAnchorPoint.Top, + }); + + sciChartSurface.annotations.add( + gammaTextAnnotation, + gammaMagTextAnnotation, + impedanceTextAnnotation, + resistanceLabel, + reactanceLabel + ); + + // ═══════════════════════════════════════════════════════ + // UPDATE LOGIC - Recalculates all visuals when point moves + // ═══════════════════════════════════════════════════════ + + function updateInteractiveElements(gammaR: number, gammaI: number) { + // Clamp to unit circle + const mag = Math.sqrt(gammaR * gammaR + gammaI * gammaI); + let gr = gammaR; + let gi = gammaI; + if (mag > 1) { + gr = gammaR / mag; + gi = gammaI / mag; + } + + const gammaMag = Math.sqrt(gr * gr + gi * gi); + + // Compute impedance: Z = (1 + Γ) / (1 - Γ) = R + jX + const denom = (1 - gr) * (1 - gr) + gi * gi; + const r = denom > 1e-10 ? (1 - gr * gr - gi * gi) / denom : Infinity; + const x = denom > 1e-10 ? (2 * gi) / denom : Infinity; + + // Update draggable point position + pointDS.clear(); + pointDS.append(gr, gi); + + // Update highlighted R circle + populateRCircle(rCircleDS, r); + + // Update highlighted X arc + populateXArc(xArcDS, x); + + // Update |Γ| circle + populateCircle(gammaCircleDS, 0, 0, gammaMag); + + // Update text readouts + const signG = gi >= 0 ? "+" : "−"; + gammaTextAnnotation.text = `Γ = ${gr.toFixed(3)} ${signG} j${Math.abs(gi).toFixed(3)}`; + gammaMagTextAnnotation.text = `|Γ| = ${gammaMag.toFixed(3)}`; + + if (!isFinite(r) || !isFinite(x)) { + impedanceTextAnnotation.text = "Z = ∞"; + resistanceLabel.text = "R = ∞"; + reactanceLabel.text = "X = ∞"; + } else { + const signZ = x >= 0 ? "+" : "−"; + impedanceTextAnnotation.text = `Z = ${r.toFixed(3)} ${signZ} j${Math.abs(x).toFixed(3)}`; + resistanceLabel.text = `R = ${r.toFixed(3)}`; + reactanceLabel.text = `X = ${x >= 0 ? "" : "−"}${Math.abs(x).toFixed(3)}`; + } + } + + // Populate an XyDataSeries with a constant-R circle + 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)); + } + } + + // Populate an XyDataSeries with a constant-X reactance arc + function populateXArc(ds: XyDataSeries, xVal: number) { + ds.clear(); + if (!isFinite(xVal)) return; + + // Near zero: draw horizontal axis + 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)); + } + } + + // Populate an XyDataSeries with a circle centered at (cx, cy) with given radius + 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)); + } + } + + // Initialize interactive elements at the origin (Γ = 0 → Z = 1) + updateInteractiveElements(0, 0); + + // ═══════════════════════════════════════════════════════ + // CUSTOM CHART MODIFIER - Handles click & drag on chart + // ═══════════════════════════════════════════════════════ + + class SmithChartDragModifier extends ChartModifierBase2D { + readonly type = "SmithChartDragModifier"; + private isDragging = false; + + modifierMouseDown(args: ModifierMouseArgs) { + super.modifierMouseDown(args); + this.isDragging = true; + this.handleDrag(args); + args.handled = true; + } + + modifierMouseMove(args: ModifierMouseArgs) { + super.modifierMouseMove(args); + if (this.isDragging) { + this.handleDrag(args); + args.handled = true; + } + } + + modifierMouseUp(args: ModifierMouseArgs) { + super.modifierMouseUp(args); + this.isDragging = false; + } + + private handleDrag(args: ModifierMouseArgs) { + const { mousePoint } = args; + const xCalc = this.parentSurface.xAxes.get(0).getCurrentCoordinateCalculator(); + const yCalc = this.parentSurface.yAxes.get(0).getCurrentCoordinateCalculator(); + + const dataX = xCalc.getDataValue(mousePoint.x); + const dataY = yCalc.getDataValue(mousePoint.y); + + updateInteractiveElements(dataX, dataY); + } + } + + // Add interaction modifiers + sciChartSurface.chartModifiers.add( + new SmithChartDragModifier(), + new MouseWheelZoomModifier(), + new ZoomExtentsModifier(), + new PinchZoomModifier() + ); + + // Preserve aspect ratio to always show circle (1:1 aspect ratio) + const xAxis = sciChartSurface.xAxes.get(0); + const yAxis = sciChartSurface.yAxes.get(0); + + sciChartSurface.preRender.subscribe(() => { + const result = preserveAspectRatio( + sciChartSurface.viewRect.width, + sciChartSurface.viewRect.height, + xAxis.visibleRange.min, + xAxis.visibleRange.max, + yAxis.visibleRange.min, + yAxis.visibleRange.max + ); + + xAxis.visibleRange = new NumberRange(result.minVisibleX, result.maxVisibleX); + yAxis.visibleRange = new NumberRange(result.minVisibleY, result.maxVisibleY); + }); + + return { sciChartSurface, wasmContext }; +}; + +// ═══════════════════════════════════════════════════════ +// HELPER FUNCTIONS +// ═══════════════════════════════════════════════════════ + +// Preserve aspect ratio (ensures circles remain circular) +function preserveAspectRatio( + width: number, + height: number, + minVisibleX: number, + maxVisibleX: number, + minVisibleY: number, + maxVisibleY: number +) { + const visibleWidth = maxVisibleX - minVisibleX; + const visibleHeight = maxVisibleY - minVisibleY; + const containerAspectRatio = width / height; + const visibleAspectRatio = visibleWidth / visibleHeight; + + let newMinX: number, newMaxX: number, newMinY: number, newMaxY: number; + + if (containerAspectRatio > visibleAspectRatio) { + const newVisibleWidth = visibleHeight * containerAspectRatio; + const widthDiff = newVisibleWidth - visibleWidth; + newMinX = minVisibleX - widthDiff / 2; + newMaxX = maxVisibleX + widthDiff / 2; + newMinY = minVisibleY; + newMaxY = maxVisibleY; + } else { + const newVisibleHeight = visibleWidth / containerAspectRatio; + const heightDiff = newVisibleHeight - visibleHeight; + newMinX = minVisibleX; + newMaxX = maxVisibleX; + newMinY = minVisibleY - heightDiff / 2; + newMaxY = maxVisibleY + heightDiff / 2; + } + + return { minVisibleX: newMinX, maxVisibleX: newMaxX, minVisibleY: newMinY, maxVisibleY: newMaxY }; +} + +// Create a circle as an XyDataSeries +function createCircle(wasmContext: TSciChart, cx: number, cy: number, radius: number, points: number = 360): XyDataSeries { + const dataSeries = new XyDataSeries(wasmContext); + for (let i = 0; i <= points; i++) { + const angle = (i / points) * 2 * Math.PI; + dataSeries.append(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)); + } + return dataSeries; +} + +// Create a reactance arc as an XyDataSeries. +// For reactance value xVal, the circle has center (1, 1/xVal) and radius 1/xVal (positive) +// or center (1, -1/xVal) and radius 1/xVal (negative). +// All arcs pass through (1, 0) and curve to the other intersection with the unit circle. +function createReactanceArc( + wasmContext: TSciChart, + xVal: number, + isPositive: boolean, + numPoints: number = 200 +): XyDataSeries { + const dataSeries = new XyDataSeries(wasmContext); + const radius = 1 / xVal; + const cx = 1; + const cy = isPositive ? radius : -radius; + + // The reactance circle intersects the unit circle at two points: + // 1) Always at (1, 0) + // 2) At ((xVal²-1)/(1+xVal²), ±2·xVal/(1+xVal²)) + const xVal2 = xVal * xVal; + const xInt = (xVal2 - 1) / (1 + xVal2); + const yInt = isPositive ? (2 * xVal) / (1 + xVal2) : (-2 * xVal) / (1 + xVal2); + + // Angle from circle center to the second intersection point + const thetaOther = Math.atan2(yInt - cy, xInt - cx); + + // Angle from circle center to (1, 0): + // Positive: atan2(0 - 1/xVal, 0) = -π/2 + // Negative: atan2(0 + 1/xVal, 0) = π/2 + const thetaOrigin = isPositive ? -Math.PI / 2 : Math.PI / 2; + + let startAngle: number, endAngle: number; + + if (isPositive) { + // Sweep counterclockwise from thetaOther to thetaOrigin + startAngle = thetaOther; + endAngle = thetaOrigin; + while (endAngle <= startAngle) endAngle += 2 * Math.PI; + } else { + // Sweep counterclockwise from thetaOrigin to thetaOther + startAngle = thetaOrigin; + endAngle = thetaOther; + while (endAngle <= startAngle) endAngle += 2 * Math.PI; + } + + for (let i = 0; i <= numPoints; i++) { + const angle = startAngle + (i / numPoints) * (endAngle - startAngle); + dataSeries.append(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)); + } + + return dataSeries; +} + +// Create a straight line as an XyDataSeries +function createLine(wasmContext: TSciChart, x1: number, y1: number, x2: number, y2: number): XyDataSeries { + const dataSeries = new XyDataSeries(wasmContext); + dataSeries.append(x1, y1); + dataSeries.append(x2, y2); + return dataSeries; +} diff --git a/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/exampleInfo.tsx b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/exampleInfo.tsx new file mode 100644 index 000000000..0b9b36245 --- /dev/null +++ b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/exampleInfo.tsx @@ -0,0 +1,66 @@ +import { createExampleInfo } from "../../../exampleInfoUtils"; +import { IExampleMetadata } from "../../../IExampleMetadata"; + +const metaData: IExampleMetadata = + //// This metadata is computer generated - do not edit! + { + reactComponent: "SmithChart", + id: "chart2D_modifyAxisBehavior_SmithChart", + imagePath: "smith-chart.jpg", + description: + "Demonstrates how to create a **JavaScript Chart with Smith Chart** using SciChart.js, High Performance JavaScript Charts", + tips: [], + frameworks: { + javascript: { + subtitle: + "Demonstrates how to create a **JavaScript Chart with Smith Chart** using SciChart.js, High Performance JavaScript Charts", + title: "JavaScript Chart with Smith Chart", + pageTitle: "JavaScript Chart with Smith Chart", + metaDescription: + "Demonstrates Smith Chart on a JavaScript Chart using SciChart.js. SciChart supports unlimited left, right, top, bottom X, Y axis with configurable layout", + markdownContent: + "## Smith Chart JavaScript Chart\n\n### Overview\nThis example demonstrates how to create a high-performance chart with **Smith Chart** using SciChart.js in a JavaScript framework. The chart positions the x and y axes to cross at the data value (0,0), producing an oscilloscope-like layout. It leverages the advanced customization available in SciChart.js for axis placement, as detailed in the [Central Axis Layout documentation](https://www.scichart.com/documentation/js/v5/2d-charts/axis-api/multi-axis-and-layout/central-axis-layout/).\n\n### Technical Implementation\nThe implementation begins by initializing a `SciChartSurface` with an integrated WebAssembly context, following guidelines from the [Tutorial 01 - Including SciChart.js in an HTML Page using CDN](https://www.scichart.com/documentation/js/v5/get-started/tutorials-cdn/tutorial-01-using-cdn/) documentation. Both the x and y axes are configured as inner axes by enabling the `axis.isInnerAxis` property and aligned to the center by setting the `sciChartSurface.layoutManager` property equal to a `SmithChartLayoutManager` instance with data value based positioning. A dynamically generated butterfly curve is rendered using the `FastLineRenderableSeries` and utilizes a fade animation as provided by the [Animations API](https://www.scichart.com/documentation/js/v5/2d-charts/animations-api/animations-api-overview/) for performance and visual enhancement. Interaction is further enriched with modifiers such as `ZoomPanModifier`, `MouseWheelZoomModifier`, and `ZoomExtentsModifier`, which support smooth zooming and panning.\n\n### Features and Capabilities\nThis example showcases advanced chart capabilities such as real-time data rendering for complex curves and dynamic interactivity. The efficient use of WebAssembly ensures high performance even when visualizing large data sets. Additionally, the chart demonstrates clear separation of concerns by isolating initialization, axis configuration, data rendering, and user interaction, which allows for robust customization based on the application’s needs.\n\n### Integration and Best Practices\nIntegrating SciChart.js into a JavaScript application is streamlined by employing a modular approach where the chart initialization function is directly invoked with a designated root element. Resource cleanup is handled by returning a destructor function that calls the `delete()` method on the `SciChartSurface`, in line with best practices suggested in the [Memory Best Practices](https://www.scichart.com/documentation/js/v5/2d-charts/performance-tips/memory-best-practices/) documentation. Developers looking to extend or optimize their chart applications can also refer to the [Getting Started with SciChart JS](https://www.scichart.com/getting-started/scichart-javascript/) guide for additional insights on integration and performance optimization.", + }, + react: { + subtitle: + "Demonstrates how to create a **React Chart with Smith Chart** using SciChart.js, High Performance JavaScript Charts", + title: "React Chart with Smith Chart", + pageTitle: "React Chart with Smith Chart", + metaDescription: + "Demonstrates Smith Chart on a React Chart using SciChart.js. SciChart supports unlimited left, right, top, bottom X, Y axis with configurable layout", + markdownContent: + "## React Chart with Smith Chart\n\n### Overview\nThis example demonstrates how to create a high performance React chart with Smith Chart using SciChart.js. The chart leverages a custom Smith Chart layout, showcasing how axes can be positioned in the center of the chart by setting the `axis.isInnerAxis` property and using the `SmithChartLayoutManager`.\n\n### Technical Implementation\nThe chart is initialized through the `` component by passing the drawExample function as a prop, a pattern that follows [best practices for React integration](https://www.scichart.com/blog/react-charts-with-scichart-js/). The drawExample function sets up the `SciChartSurface` and configures the Smith Chart by using the `SmithChartLayoutManager` with options that specify data value based positioning. `NumericAxis` instances are added with the `isInnerAxis` flag enabled to ensure that the axes are rendered inside the chart. For more detailed information on central axis customization, please refer to the [Central Axis Layout documentation](https://www.scichart.com/documentation/js/v5/2d-charts/axis-api/multi-axis-and-layout/central-axis-layout/).\n\n### Features and Capabilities\nThe example features an efficient data series generation that calculates a butterfly curve and displays it using `FastLineRenderableSeries` with a fade animation, as described in [The Animations API](https://www.scichart.com/documentation/js/v5/2d-charts/animations-api/animations-api-overview/). Interaction is enhanced by including several chart modifiers such as `ZoomPanModifier`, `MouseWheelZoomModifier`, and `ZoomExtentsModifier`, providing dynamic zoom and pan capabilities which can be explored further in the [ZoomPanModifier documentation](https://www.scichart.com/documentation/js/v5/2d-charts/chart-modifier-api/zooming-and-panning/zoom-pan-modifier/). Additionally, a `TextAnnotation` is added to explain the chart's functionality.\n\n### Integration and Best Practices\nThis React implementation follows a modular approach by using the `` component to encapsulate chart initialization, making it easier to integrate SciChart.js into larger React applications. The example illustrates proper use of inner axis configurations with `NumericAxis`, supporting efficient performance and clear visual presentation. Developers looking to extend the feature set or optimize performance can find further guidance in the [Numeric Axis documentation](https://www.scichart.com/documentation/js/v5/2d-charts/axis-api/axis-types/numeric-axis/) as well as the [React Charts with SciChart.js article](https://www.scichart.com/blog/react-charts-with-scichart-js/).", + }, + angular: { + subtitle: + "Demonstrates how to create a **Angular Chart with Smith Chart** using SciChart.js, High Performance JavaScript Charts", + title: "Angular Chart with Smith Chart", + pageTitle: "Angular Chart with Smith Chart", + metaDescription: + "Demonstrates Smith Chart on a Angular Chart using SciChart.js. SciChart supports unlimited left, right, top, bottom X, Y axis with configurable layout", + markdownContent: + "## Angular Chart with Smith Chart Example\n\n### Overview\nThis Angular example demonstrates how to create a high-performance chart with **Smith Chart** using SciChart.js. The chart positions both axes in the center by using a custom central axis layout and inner axis configuration. It leverages the [scichart-angular](https://www.npmjs.com/package/scichart-angular) package, which simplifies integration with Angular projects using the `ScichartAngularComponent`.\n\n### Technical Implementation\nThe implementation initializes a `SciChartSurface` using the Angular standalone component, which passes the chart setup function through the `[initChart]` property. The chart employs the `SmithChartLayoutManager` with options set to position the axes based on data values, ensuring that the axes cross at (0,0). Both the x and y axes are defined as inner axes by setting the `axis.isInnerAxis` property, as detailed in the [Central Axis Layout documentation](https://www.scichart.com/documentation/js/v5/2d-charts/axis-api/multi-axis-and-layout/central-axis-layout/). A `FastLineRenderableSeries` is used to render a dynamically generated butterfly curve, with a fade animation enhancing performance. Additional interactivity is provided through the `ZoomPanModifier`, `MouseWheelZoomModifier`, and `ZoomExtentsModifier`, which offer smooth zooming and panning as described in the [Tutorial 03 - Adding Zooming, Panning Behavior](https://www.scichart.com/documentation/js/v5/get-started/tutorials-js-npm-webpack/tutorial-03-adding-zooming-panning-behavior/).\n\n### Features and Capabilities\nThis example highlights advanced chart capabilities such as real-time updates, dynamic data rendering, and a unique oscilloscope style layout. The efficient rendering of complex data series, such as the butterfly curve, is optimized by the use of `FastLineRenderableSeries` and `FadeAnimation`, supporting high performance even with large data sets. For insights into performance optimization, developers can refer to the techniques discussed in [Performance Optimisation of JavaScript Applications & Charts](https://www.scichart.com/blog/performance-optimisation-of-javascript-applications-charts/).\n\n### Integration and Best Practices\nFollowing Angular best practices, the example uses a standalone component to encapsulate chart initialization, which promotes modularity and ease of management within larger Angular applications. Developers are encouraged to consult the [Getting Started with SciChart JS](https://www.scichart.com/getting-started/scichart-javascript/) guide for further details on Angular application integration techniques. This example provides a robust reference for implementing complex axis layouts, interactive modifiers, and performance optimizations in Angular environments.", + }, + }, + documentationLinks: [ + { + href: "https://www.scichart.com/documentation/js/v5/2d-charts/axis-api/multi-axis-and-layout/central-axis-layout/", + title: "SciChart.js Central Axis Documentation page", + linkTitle: "Central Axis documentation", + }, + ], + path: "smith-chart", + metaKeywords: "multiple, axis, chart, javascript, webgl, canvas", + onWebsite: true, + filepath: "Charts2D/ModifyAxisBehavior/SmithChart", + thumbnailImage: "smith-chart.jpg", + sandboxConfig: {}, + markdownContent: null, + pageLayout: "default", + extraDependencies: {}, + isNew: false, + }; +//// End of computer generated metadata + +export const SmithChartExampleInfo = createExampleInfo(metaData); +export default SmithChartExampleInfo; diff --git a/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/index.tsx b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/index.tsx new file mode 100644 index 000000000..aa8d648b1 --- /dev/null +++ b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/index.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { appTheme } from "../../../theme"; +import { SciChartReact } from "scichart-react"; +import commonClasses from "../../../styles/Examples.module.scss"; +import { drawExample } from "./drawExample"; + +// 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() { + return ; +} diff --git a/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/smith-chart.jpg b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/smith-chart.jpg new file mode 100644 index 0000000000000000000000000000000000000000..351db0f9e884fe85c9a5c2722e2d47beb0a3be0b GIT binary patch literal 43248 zcmbTdbzD?k7dCu`p+q_aB!-j{r6eQc6j7_W#=P$kjRlz##wCSpPf!|H>k= zeDT@>?1=^sUJEA|@H)Yj1pv)Au1;6B2WV5ffENYYVOO^GKX%%cZT^p4{7u5hD5(Hk>`2Wtm+5vzc8Q|sZ{O=s+UGV)R0JK;+nYoz#GamRGa*X@h zN?7Rcd&u=F0KkjCygW1jHxv&5U`8)5FRCstF*yJLodAHQo={B6|#HoCu!@v5=>w?Xjm@CXSANeKu@S#MC@VEzBxE~ml0eD|^m zpdf^xaCV>&HUNhL0;Palwn4yYL+~Jf+kYkr-Y&R!_ymO4h^~_ZI1nfl2N#NmhYRjw zi0{>{giC=($tw5&pGwP&fX$g&C?G1Gko{pvGmUot9*3~GOW-x48?(=9iY0S5#J2*VMMOwzYS3c6Ij*3=R#CjE;>@%r7i1Ew8Mut#9lf z9HNenPfpLy!Tb52>43lgnbH3-9|~|jIJme_T!O3lKycoG2b2OAk5v$#@_`nCnKKoe zPyiwI!>II<=4cm@ zN&&!uQ*0>Dt3e5z_>`zROgk5IU}BiFY+xvN7nIAN$G^Osq9T-;utPC7cU>i|3|A}K z3xa_Y0V+Jy!SvdM*Xxiw5to35*K+}l7fmwb>u1L~EE%Cs0UjIZ_uNlQPkcfYo(NZ2 z@Q{45X3&u6w9|xgXW>%BH8Yi{Z)v>f@GWaXU+4fC5#!s*jS<4gZ<1bG9gwZ@&IWPu zbJsMyj8JaipW`XOot@!N#GRdno1GE=3B15GoPUlSh&;6>OGbPsz@72m1D{$;9{Bgb zA>z&m_#n^Ed1Zh@WCXmU)>6WW20x*q@{}6_eoT%s^r;FsDck>i9XEuBXDAkk)skl+ zy6RK@?{J*d<*~t>)LOE5ocx?DSD(U1T=t#eT&lQ6#;j~5!Eqk#nR_R$rFKd0OsJEkZ7 zcD8>wbU1ga^YVUDXjh&(;4qA z!i{(M*=1ecLnbr6zbB1)dhI;x;EV>>Rm52$rjV`KGLLW5Zg|hOJrmO&W#7v|62G3= zd_#t?Vn#%JH}dMXrV51TTi#s?d|2q?tlL1>dMCzy;p^Dr8`;^Nwik7pj|T~No>>Rp zz02Vl5!JEeM4J-$Bf{Ropr#S2S6yEdy~AI}<+)^=U_Cv_YQGsV#?F*4`B+u^`wwhi zNo~r^($24bmHng`hMH8 z6U3&OuniDUT7>rLUfhy#)egE{;<=R0DwbTs_r*cduR7nLk5T%mu=?Iily~~Z zV`n$1J1R-^EQd{rv30^KEkgbPgusc>;@(OuK=8oG$U-u~bv=4gvob}Atwt|Sn7ZlH z7*&{nc5c{`X@qQX8cJ1(A8ot1QW@`Yt2AOp=`+r*t0|?zC`C)t?kt;`7&H!-x#b5` zwLVC_A>w#tKyvB*YIod>2!|_2XYkzdnin(guNjt+TO%v`H>bMsPzO>){AK0D*N=!a zgY`N9`ZZu%MXRCpW63P~Vc+giG0Y>{m$KV^tdr?zmQov7u~A^c+e^qav4_(GPS*N`zM&TLtd$bIL52J_JKV=9SYPtBEhBWQi#sFUpXlk8@8GckK%apN`Uput zOP;z3{7b+!Re9>GH-MI^JOJM6ngahiAo9N_Sr&HLe}*5*U97fv=c!TFri%&k*7P#nk#?FN<8Z5OPHV2wwDG;>_<2iKi-Th_ z{vP6;)vA}prfVNP7y}yq=o+7xcl*P6BfQh^6%~Cq_D!@jz688~xm*Il!lB(?#a>`+ zT$I&yN+&LX5I&5ZL{XIM*e?E?AH5Vz-}A!YDBfaXpnx@Y;KI(#H`zE&hV$j9O_vjMv*LZE_X-Pe;_+1RTqHqcH z84myEdyLc+D*w%*X@} zA8((TVDaA;&+my;)xDd}jo@D5c7H2lMLN72N1PSYDf0PS0CTd{sFunf^H@0+r!>a> z8er97H{o-H{hQx|1LGFX_b0Qf607YAvls)yb|iHf_uFq)R@Z)UxN$3d)zb5WslkcG zvMxY;$8)v*5slvq--hQ3BJpA=QWqva?csBiSi049n~XZ7CAbZ*V%gu2h!YR)!fY`5 z@z`Yi8yb0R{bS3uz(CGpLw9B1QR`M7A5N?BZ?k)5EiQ$t!rDlKrsg!+c=;SJ==s&} zy~-;xv=F?zo5S2r^$u-FA+vayxmyudZDuyDnoc|?pIFSVUmol?pVQYgr#=r z|MFuVNyZI-+0VK*5ck&4g$BdJU@~0kDLU{`Y2QCSk3|c`cZj^$$eStC5S!rSoY2$f ztdSNRI$D?gcnOS)?o-}5;#1n65IU>|w@!W@`txVAy{%UXb#jQ&&F?*?MS#;3`bo%5zj{%k_LnS zT-9j3uyv=oOJGo08cq~+p{mMat@?5A^8@U?Wa~=+CWwW14eMQGuB$Y;>DMCP;U)ii z^I0q11#OpyO{So>4K9Sc9k;h^;u1in6Tx|1*2JiIy!k$=rjZiFi4z<|DhO{~D^w|AF)8gJ{Ig<( z{$5U4=9cxcQuf|(VaeAucgn^9M*Wv@jq{jISEI?!SC%2?{<3g6gGr;$ zi?E6Kbpn%ar;KP zBWuT-NtiISQqyPd!amK4pW4aGT^z0WlAByxyZ_2LMf?9nH~#`BS?btwZAdxj)&#Cx z8UqNK{=raJK1&-s4I(XV5E%W7lCnTACkVQ_v@0kji?|&4>$5Z$#mZB!pT<0U_f6!P z=+3c-{kx($PgzHeC=Hq6@Y_O@J$vfGV&S1e-<$Py0tuxF*>T;AjZf|8i>Vrtycab{ zCLa5`+00s&1hWZLssES~D!E>X3%}!GOUP(n%vl?{LZ1`3CDhAMt3Y(*l0pBGw-OV#p0(-w>r_pEWvVHjS47#}K#dTq$5J-4uMT82%EqTUWzb}`JMQ== z>@+LK2s_&Emw**5?JapjV#QZyVea1s1Ot_uB&Y}R>qr|0cTsgQ)?X)ozPj(Prqj_c zPAnw!4MNW({#E2RpU#NGyZESA_ogOQJ)IN=M29=nshRN=rSFre9L>T!gxhJcB7Gts zKM~X3&q>W0Z|58g&+#Z4enGnEc}8azK_*iUy5mX>Vn4pU^VZ|v`ZSfI$C2OhS5z4i zp&2E77vC@FF&0d`#;95zIh}4hSAIwBSMO1q*OP+FasQM&+%a!v*@d`dkt>jr5k!y( zk+AF4404VXf%~R|l%rZHIbEY5pxo3#YQMud%H)~td0c$}5YtPKsT1MWQI^^uOaVKeX zBEKK@uwV&pg^aFsmTg|2NCGCJ_&auR2qwd)EZl`VycVUbC+a2JtC%4PtzHj3+#K5% zTISArZ5|>eO1UUl>@S?l=+2x|jw-h82}@SK5c@$7=k&{O-l%$=FZ#j$g;Z7l(!C!x zmQn8h>JP2`3A@*XcpDPkq$4uz-@FRoaQP<2R(s(7r64fN^n?b5|Is@4=b|I?n)8E( zC)(57x928;7q<>LN{w=5X%55!(Dc;Ve58Ukhvg-zxW%|@{oM@ImTtT~Kb!H?30d|Y zFzQYE!u0FDr|MOdr5LHI-%4h)ijWaL6ksrb;;yDOi_@$(hz)#0TPzk+Z#=cE?8PKF zBvjegnO8+Q62Cq((hYVL5xCy8SmY(vnzVWSE`iNN*`r%kgTV#(Ia>xlD}s%?er>3$ zPLX_|yMFRIA|oax;XC&$iUTo1M>pxGj7vg6Wr( zRSn&>TMeApvbtvZAzA`o_&^X~mY~~^U^M90nX=vU5%6BiIJ~`s=EDmsi+>ilgAL>D!#^;y+8ns$R$+S&0+L zloJ*h6qf&-exMd#vLbRZP;!kDL}IG}D0-Q|@*DhY%}k}W;Vs15OGbActrB2+H1Ob8 z_fItIhX^k$-wn$w05s%Yyu{pSJm48ruJoe4f(J47OMelIcQU&-L6(=({cS@ngPYFi zVJ;hk^YSg<;{)FV&q6Pru{)}_ZK<~2o6F=AKnW%?3OGFDo9o)#FM%cvjjmxYT+S!( zd-?m=qW5TqsPiDbef^@ir9=Kcs(KITuqUj?pI3W0wJh!uc={7ovUx_D8o>kaH@Fx; zEC!u_29zZNZ=r}Aqk50>9>%nUVR+Abv0*J7kH&glheeXnb@w7(esgdj>^LAuoAZ;y z35c06GUoqf+0kUp7)Gif6Gt$%L%Yl?zPNkWu_H75Sn1m|bj3`gT{(y}Hkn<*( zejJL#dwjdF%F2T-^KQ?#w*`2N#1w(|^*(y?pCovcl+a`4B7(k45fv{#n%!8UM>$s= zt)uhkb1~(k#;?c~y*s2@FTR#s0?)|ZQ7_m6awbe*r!g8ez34h)`v{X$sP#|8FAG#* zZlK}l>$^WO^IhmpnwttQ%x+!+v{3Yg_II++^hMdCCxrb<^;*sPuzIH|X8<{ZE1m!h zWBp3zApVA24uau|=hcvS$ESIVgmAj^QXZ`N7m$`UCG~fH<`h0Xu> z@kjTjKf^9YYxw=T76&KANy&+4m5buXy)-YCi->u(;zTp~2_I<&1W*EA{-I!;m&o;W zE`4jMotefNp0a-sY4SPy=gwRlM8vw zx*^6rel@N7&5h~u%~O%0s^`hlx1C+f`@^8y>fZ*N@2-CiJC{d!y(@Q8}S1X4}5 zJht!uMKIt=V=LosQjL5Jpu#)KwqlKfhEqUyn8nOYySiN-O&6BT4funPXHZ zwftR86b+GxB>yw5gH|r*KL!NQ^p&`Q>oOtt7{jRpA8o+@@C+)jJI3q?Or`Fm?yVcq zdOeH1FGYJi@M9n{*y$!a^>YXJI|REB@kJ@WxCHXUOwqX)4F?FV(|~gwn$RKbp^}zA zV!lJz{q;*=^qk22##CM`-yZUyst-)^sFy2a#C_O;!hbg{Mo1%w-Xs!B$)@+7Od&Ye zQNL+PPaqDKoJl(+GMTw6@Hi+#k|iUQ_2kT851P7+%BI!%Cd!nb#Z+)m3{xp?rzVWH zG+ch6Q8(mE#*WI{XeBw`E>TuWhFKK?=yRHgOPZE;LfC|PZi-6kyfaI!m5GxBTyYAG4|#e|bS{Ah)WjyGU{3t{$Yeh!=~cib zaC>r(9{VIuv^XsnTJ>&my&dP4rbXbhDM~%TK9Jz-((`J#<6u$o_*;{slpBnjZf-xq zOPATt4{bBM$B0-H$LbQlV_KBV$&-t zuKvnVJdN~)!ln*uCC=8n>Rx%fK|{``n)tbs+q-63#e7Pu?{_R<4SKNIZMJjkl@6y@ zGAHVve``Day!Ru7q8BDu0&CqUl>FL!zK|J5Ie+r>OGIc$FyIO;LBo07;xK>FHthr*7;igf8|O<|4DXL{F1&LXK_KdZxRjkri5r@lTjq-V!Sl)l)OKj=54V2U_XAP+*-jpL273u)D1l z$J=gb`n8$)t+TR@)-d~%1upSC<+aA1*=RJ$H8*<&8^X|L)St37m5DMr_J9llRi$*@V(m=ruY8jYIYSdY-QMMg1ZV#|6_V8!n5qGiG-W7 zk6xfz;9_nX7+;Ou%o- z{(?=FG6QnhZQhr{mc=G7?ba;>L~tXIMEiVFM7dY5Yt{!4%H3taK<4?&m+ z(71QByjnGVXA!|#`^4mJxUbO}AiWA=6gOXXXrNSh_MGyrQ`J=I0kJVJWL>|{i##Uz5RKJ?$Gvjcor>bWCEof9lH~2Sr=Cx zZp7Oo{0!LowL{np8@h3ud;5r~z9H^uK*G+w$Lc%N_IJK9E9q6?N;;9vL3W$Cpx+tw zIEamLFUA+CPcwvE{B)TNQLxp4lot?U&G38$x)aAe<`)0k>fc$YBAJr%i>LH=#Ieud zaXQh8aaA?l7=k``>a|4OI7D?T0rRvVDyN&_(TdncLozpc&u1Jlx}6_EwYorw%w4~!!5v&^XrCrRl9n-^BEA(uW9NHkX{oWW2FQ5$9 zDcvU5&)A-*9%+{nlp=}Yg_YAyVwi9Z(T09p=Xz$H>Vktb=8LOla5QQOMB(@QaB<@$ zKP{c7ytMVRS&W%9J1@QjjA-zlRaQlNSnAP1iLcFBx}(DZ?E=7|-0_$Z)*KAazBTE3 z@yk-?Lle3eMPqfXVuQ!wQT z7b$hwF+x5crd!OVZc6P5`SJ6V&+m(QZnAx&`{eAcd%a!KohS8pT3?qtymEFHaWRM3*8^5_*HweoY$)92s|1=FUO|F4evo}Sr$n&op=04L zx^Uy-LmG~KmB{9L!k$X;OTcE>4kjnt{qDVhpCfvd;`%u019QjPz~u)%gtVVHJj7gP1^(6||9ro*f`uh6 zH`KE3UFbWOwl1adsx*T9sHdiK-}Z1?^HMW9oe;9osm(@XBjQ36mEx`L8-)aGHi8`a zPnla|;TL!FFg7^tDe3t(Co(T$2aF!mdDP1XI01CJU520~r^CE*<-4|S0b#oL9U zeXnH!Vu(MV?eNN5GyBHk%^l&e+f#K(CnVVn3gjOh#T*$}-Rv#|7w;L!dt{8*Ehh$c zl!mvV=y9Q^lzexRH6}0iKZoj9&E2)$sh--bkkqgCl~j~B%$eoAb0Ds7R`CV78~L<5r;R_}MY&=m`D{U2 z-7MS3wC57gYAN0;W^RJGN%l%*C)aw4Mk4Z&5}Rl{MdIx#6f~gyh2HzmqHQ{R^`8)A zpc|Py$Xg1}@C`?{rZdkBb+d!jSbs<-dzjwgO=Y^;7rdFf0q93>OdL&A^53WXbj$3T zVRo@qMUvZ;iTXNW-vGKf$Ru+tFdBpCk@d6Quq>lO7m4SRP@>2!t>wZ1Ybad-fY z83IA~@wCqjU{cJTpFGGvGQUR=AQeK_tj9=&h0Y$N;VV)pU3b;n=uEo=WCBxHZ@(&? zvOlsoK@Nh9uC}p6GH#D({+_O@wl6_?zH3uF?ISz*lvyxzaB}^zt6+)3_L(2^P)`l@ z<2SGbx?fQd@Trq6oJ&wrlxtu=Z)+jgKw%Wib%^66OBS#tPLk}BD+IQc=|*1vHK_aSUZ!$cDU#za8r&eaAodx zcw+{08gupYEbk#^7g!^;HKtD&Yn~fw*rr_-{i*4B(G(K#GRTKuR~~s{h+O>)e4e&j zN`~S;_>vMPkSPugnjFaxNK{{2dUBmPnc+Yj!)qg7oWQr-w>2S-OVY_yv%Ri|_D4V3 zz|Oj275q8;e=?j=?!#_(wX)f2ntJxM}RuaEs|z=6bMN+WQOi%qyr1 zcieYH<&Rgz~>xRp#aS(`~+(NPGq5r3+s+CSR`x9(dIVQ2iY!4>mvBbln#E3bLe zU327fUV$_P%WaO+T>`bQxRz4+Zm~xayEa*@`I2ylsjE@>l%g1_2{7>|C=vhswf#Cp zwzwCZDp7%ibAgiPNzbn7k1XK`R;~_M2))AKu^aY(+5W#02gK#b87WwS)Lpo$G2H;2 z`Bk9_bUwB@X{eXh04U1qJTG4x^hE^2+fa-+lxGQ#tL$&!<7+EUay);Rkj0CrPHo?qX{8AP z3qNf%=3GdO@m%cjM#CbSm*^?r-JvtZ{MHo?g0m=eifhmk0ZeBNUZiSgSdfXxhY4S} z)17R=TRtL_Na&|2!wWj6k7Yv6`taPXUe`2yafSAIYlA`#3C8rUbBZyfig()e6|N~M zU$S&;9F6VMo=Vy(zKmQr%EVkv$4 zRPm)XjL%_S!TuZ{&{f0=iVqty2jZ2E=M!S#uAm|&FE1a(`+@F43p1HQ+jueM3j5`n zMzr5XbQa;BhVCsz9^k>Z?^GN5$v+Po#q63aE5BYjYf}w$60F);tQ4F`OS_gM54v00 zDMz&HiAif!#RGj7hwlPC-5t_o<^)v-UpYS5EeXmn%P&S~`jR1*WyvbuD$^?)nadC{ zXMLYDbq24Pyjl>avMIL!LAyG+#*1&|etVVUjLw1-(IPX+^h#8r0 z2to588Q4Ef=81pR#?ko|xuu~qyCw;G#A<7tDdm@HMgs!dN)SM4>?F)TDJB{G3$}IYpfMYsLR2zk}pAU@qn$ zLZ&*`ld#cG_xz3__KRP6mEC~qYDssL+-t0twRjHaj~$^r=gmI9Y793{^NoygPxsao z|2snX;u5Glu?c^ij^8`Kkr-U_LPg@|a#_)N&H85#935Kwh7l+|?1i^eH@2FGUNc2%irvxjsp_jG2VRq&vv$p>PV$z?xAn%_Y!~gtnnKdz@Ec zD%D&w1gjgPS|2Fz)J&}d7b1WM_Vb=;edBnHVBAl!X$NsA(}SZtLAxnn6V-~?7xAy$ zpKklg$i=F2n>={AW{FfVB2MF1c*J;S^N~sHRdS*6nvvfLYh2LWvcup+8_=sB7|Zaz zQGhCUZV_#R!@Us6G!otjjT8rQulS>#MujO8H)KGu5(?j>wESD`ib++-hCq=Gfi z6C>hObJ}+NaVH50?8!+;jzl~AQWQxmDL%z{n6G#70fuLBDeTPS zGetK+V9la97M8b=bYd=kHqRB+^a4J3c2ibGt+}N{aQfi`#pk(95=XEWZ&ZOne@?oC zTha?XQ`x$352Eb>d8TL$v|IJAUi@7_?Q8Vzrk|GAXvgaTjHF(5F-h3u;>Xu%JkAWL z6yYIn9BJvK3c$4g>CN!yC%=m9W?qzUuE=6~T$@pra6U4Vp0c$oO+}jdm8Jr8Hd$bO zGAoPlO1(t~3M&3lS6me+|0*l43Y1r(E`SFFvSgQ||8ZnjDC%$395hU?eAg9@{p-K* z{`F>Rp#Rc=*ns3Rm!skL4deHfni5nuA31#&O#@l=??ZrzbV1s%v?;XVG>RQy?y zcsi#c`!JJSvu(8q#gCHr1)2%cnLp*N+u*o|8`JYQPh`eP8=&PMs9e*g_h8R{7AjI_ z-nr!b(*mM&jN&>-Bcz})Kb*z2CThq+JOIO0UgAf}-`yhX#y{RhRWagW*O(fKB0iCY zA2TNs1WHn1y}Wdhq(f9(zv;z$oj?bf$Gco}5Vee`RS;#4@Fn}cD@(zf&mH~X3ugG} zL}vC+#_cB`1-tK=>)vU!)u~Yicd^Cx`b2>aPMeWs8znisw(}HmKHHPDq`Y#*VQ)4n z1vxs-VWX8a0Pp#u{Qb`o85eimpcbgAFJgD>XTt7NOfnBeMspBRSMJ8}RdLCDLL+y;pO7Qy zB(wzaQ*@&(LSnr$+u(2z>nVQ*!}1nRKE64XW|$BY##qC$-*gEO^@phK(=`3kKQMJZ zp`k|1$~tRFbik-e>WS|??Trs(tOtE1yL2k%CVYx(1~Km_f08rVjg5lsQ0*4)Rpp%& zEv9YqIpDSJ%U4v-I*&zo`QnLAs=kPOQsMnvxG527&Ud#+dkwpLN??5~BWTO(epHIy zdi`x=KH@ag@`wZ-K!@f@Gm5F@p(neLJ1>4zlPnlsJF?67q zoVo<6H|v2jnQq$K|C;IDW zm%#gillzaStI)nrdI_o-63#CHK|MKvyN&2^y`)m%0YRd1~^47g-h-2#s}A_Co^!*71}Q)e}#YbbYPgJu^rk zoZo=tE)EyGbt?Txn!v|vP>2=5L)}ieihbG{=D=QiK{}JF60Y{PgWbUZqk0K!<)X2% zy9rRv%Fd?F*^f-`vAbuB*}NG{lMUYr!h|jXU+K^M|e>XOz~zYaDP5#5-EC zzk-&}7wcaa7KVXkIi0}VJoZ31QU42V1}=7(B&kq+7W3xJw~hXaDZWC)Pqp_BC4;}9 zOOf6xnebictO#)v=z9MuKK@sL13j-c=!Z8z1?_)|Zy?aTl9urNLlIk)f@VTii^m1*mow%NKPr?&!fVfg7%s6p!A8;AG_KUKiuy8%Jmg(3sRUI zyoSpj&3YHP`6(fLd24Cc_nJvsh0|Cy9W1$uGYY&>^!cUbNOqA%??$@Ls(Eb{Vio?b zW|%O>o2*IuS{1i)@DUVR2H8TSr&)^Vt`LX!!33ShTx2rQ;(0vpUDzdYA4Gcqx~1QV zSk!2q=MJ5q2_C4V)_(kjy>(_V_6FXk=LLM{r1XzDrX!gmLujeR-}p%1tbb8>pJH1} zw?y)87yjI|!6+I|WJ;mHA(fn;$#G70IPk&p5auo#5uhk5%{!DQ>M4remT=px%Vkb; zrBU%`4AZK*JGh5B*GuIvuC%A!NJ-UjT-M_qd0a~D%nrER&p-Fo08?cpeW_M@Tm%)_ z!nrr;Fy50EW8dk#IoCCY^)=sGT?PjCUyP^@Zk#Q-?R5J|98G~R4_wk;h^YJn?5+TJ z6TwPXGl#bqX-hKeqcAy`$)ZE&m^Tp>l{puyhP=h#ns2?dOe}=hVP8C2IIy0#`vW6G znrLT=uDeXo8fS z!CLg^1r{p4?S z1D-H25nM1Yjviz|O_lo23{2Yly-XvL8fzuJhC#Utb~||3WR4MXs{M)a=Th$Px}6K8 z$>G2^$0N8_U*F&^A(cbe+g>BWg)z&ab=w$w zjNwkYf9(8reg)whM8-6_{Ml3CA`$zC7@3hKc{0x&NvgeGuDylgQV;h5&No6Y@4~td zup4*HHWUX3N*s3`9dq~*tWmLx1ZkaVYjC2_S<;Rh;*`W>CygO!Uf8Qu4A_y3Ca`lOlF@O|9a}V%vETQ_&j;}|WdboHq!5}E6-zGE8g?YntDzDs`SWd* z<;*M(0WjyhB~J6TqkPP^qV^(3jK%rnF-v(h<9kDkekE+|IQfT^duO3BgDKdd6y3%t z#|to`z5|{I=?-swDx{@Sj3|g!TzYMA2~@SuO%^_V@CG|Fdq?A99nn(K`kb4n!3LVf zP@**0Q+?R4k%Q`74%4S>9gP3cn7j05;Mcy$Vpq}1h<()mBiOkg=4?J1}jeVM~gbWK_-nYem^RRzZsmF zo31KG^#H9!QA2nlH@KI#^pz}N7NiXli>q2j2g)U29@~fDTC*)Ww86v*93G5jMHN?L zIkB&q2^O$pq`&lSGHyLp;QOIG=WH^cR7aydCFVv?^NK_Qwu?*D8>r z@w0<$wLE1r2-UZw@c4K6u>s|iX_C^Gc1F@f=9Eq_UwuYe1e+*K8yaHjUzl(_CBzJ3 z`3|Tys=s-Cx?+}b{yuX7;&izy&9f5u6_6+XUjPAR&|rP=e{w@8sEPhJfVhF&1GSdw zW8f-6fWZSSR)HkM^?#$tm4r#{zlzxLzH}Xr(b8|Hg_=`fQ1ncAKe*8wMta)*LdDAS z$qsx(@b(#Ye`%!rw4!f&yK|=gwbSg;A9(cLrZ3fKJ^^DHobC_6*3{w}SkB&lNF`Dz z1uuB1)hS(6_sKH$A;-idMCP;uxCBVc$6dS3YJ%B4DXLOfML>-Mk6axca(9(i_pY|{ zrmA(G#;dX|0}_G+mN>1N$8#2W+Yu5N-i1wrV}tJ-uoq?1IPcziaBVZ4_Z(gjRe;^wj zUWf=+G@A~os?aK6@pPXoJ5+JL1R&2F6BqY#AWmT#pFatP;qmoBng|98boysRr$PuO zvZsj~H}N~L2N7@(+>!P8^9|>f5&QW`z@zD`4z8)oE(JZV`esf zPi{!ROy~SyF*IEHr4x%}zR9_zhrbhAUM%4#oVKQR_VyT^Oz70W^`x2qOI3556Pt14%*ydtJ$};X)xo@ zHfCF~%)x~kH#6)iC8PY#PPS9c`Ye#yfWU9+%%4#z8DWaZIp&nbQB+jY_yhSy4xbtEp)8~T2+6zq@; zT0?Yv-ivG2W->~L{bWK`7%I>b_~Yoy3whQL+g8F}iPb>kA4I8POwWApKkfdF$6s&az1>L+V5GT@ zwixmvnjjCOy{*;yzHO-Zj@P%{gJ;p>oc;Xd^Hk3|S5`mml~VQHwSudE(Un~Ih*6qS z+!sSJHTJ5Qlz)#D#qaNjhY(cUo0l;)blI4F9g%qn6wMU2xpfPDuaQc;pL!gkg^|BM zz{537bVn}Ci#Wnya+2n@%tM|kA4#~z4j*Z%iCl!B?ozY@T%uzpD9xlbEekj5jJ&kV z#M2lSlgqq%PUN#wqO#vk36Rk^-`O3+!jIuYBk|UJGYu4xXjt>}%TO z7C}9zt^347B=#U-^!(wQGbB90WFR2opxD<4iv>%gUkdWLrn#^eI_TdImL?-^f`|s9 zt%%C(sk{VILa-Pxb6&$)YVb1RN&-w(1tpyaBML?j^9?J_MdLq&s2#us1L3?CEFYwj zV}us&b&yBYoGc_ICLx6+>XT3x%^GtgEbKmUzk5DVUjjcLbzTB_Sn=a;pL=z1#%T?1 zdB`jYLlW70HD3=a(i7}`^JHrapqBm-?k^<9+FN|;7kl=^r)l#dDm-uVmnE5|Gy-E@ z`c*|(-&&oPR)=(#BxN)6*=KCgK*zUqGtS&N9%m63puZ=VwGzd0j`0g&kJ6HzOwKPz zT^@s;?ypCU=^oQ|{F2UY^Ddgj+dc%jsy4(AX}p(@Etv8EONFTmq@i*F6)DOmsFhXJrl?i)@SXXo<{{a| zgaz`h!t0vh8o4btPxJ%B4WQJ`3_VSNku+1p%e@|Aa#7zY!3Oet-44 ze;<{+s>l5;7=l#S)w3WuIQ?M3@agB738SCyx8^Pmg*vLY3>Q&(udE-*JQ}^3fIgEw zxQlT`On1O3m6@Xx3iiQMA9hm!ym||=n*%F;Y)kSN;w7J#_#}>DI@%zo%D~MS9i6Idr&!GJ)VpW#J zkGXbYuxyeBCIn?9Ki)^HS34C1l%;4ex0!nDkvXbdNdTS4ba19fkNeBJoO%z)auItu zR@Y?q4v_iCgoEuFnyy;w$fucXyx0Ra6{-#| zq(x|qi@K~2JBp-V<6?G!XD_7c&VduqAql^LYpkE@_Xmb9hxRQL=(|~M=)SQlm8uGN zxPxyEch*oA?5jFE%a86}t^66>;Ux%yXXfdz18GSo{XSDZ-P5utuvki(5?hin{o+Yr z7lF=+(g{ii)x{!ua66cbK?Spgf}&9cI3DPna^v@t=?-=E7}_Bs&(9L<7mTSa$Q{AW zsPX!Gc%7d1jACSG%l7hzHG<*+fBL2F6*`sV)<9|a`LLJ52VIRD9C046(K5dXd{EM5 z6ETPh#qo5DNP(<9OK3)F?p|oILmi!}7=`c1VsSnav8;p_^1JqT)$Ps|lRQ{gg!FxH z-T0pl3|;QCE5yW3>5U;NlKbt760CaW4CW37OzLC^Z8%0xYG2_~O~NkK;UhBE#tG>l zH{iAGHHT5*gtBm@DFGV%K04Pw_unuMYw%KE?8v+qKHua0{kv_DcdEE6D=dF6y|b!% zCI1ipJR4?|A*3n~O#<9abZB(H4N|GIXm#$xbH9zd7stHy)DSPv(~l?4h*Yr@NHGIp_8 zcE%1_r=7dfnCwiPJ0gB@W8V$|ajXd-N)7nD*Ur#cQ?eX%kSDI_gMflsFy}5*QL>o! zpVPYsduo1m&cU#@%+qI*#m?!z=R@zi0w49naeO@9R7 z6USR##RFwuAFE3h&XT-OzLp<*3C!IaKVukm54RP3pS%gW>ZvTJRwjZoRQ~8bra&&z z)UniA2;3X`r^*CFnx~vq2_EQ18Y|B@>Ov$HT{fL7r=ge77m8DgP?dG5 z{i~`4===lTf$AFY3DGOD<6kE2idF;P{quzSRqf-d&~YVU|0fJE{hI^*Hwr*(ZNcYT z!6yQ7!8*rdP*BM)cva(o5{)%g?VZ?99m#}#33BM1wfjn1>EKHA?OCr^(5zzU7N z>0&7XjS33@<)wJkSv2#(+&N0+S}L^8MB=^F5Od?(T8guHNc8pGHM|Ep#^lyM#CVdj zsOz1mysJ90=5_Ldp*@}e$<#aCaUNWEF$7Bln~D?k!MAW8U`I`wB5dy{2@5>`c!12< z*VM%OVg#SQCA=d>Hkdk4Xf&x7e*6wHB4B9ON*n<+owQpIM_ttUwgzr@I?#f$=1`z` zbSAXqI03%-u)9OXtanAg1_Y#tT?NE$I)-ds4QF=wmdV+QkW4-8QQph_%1EQTvHFj< zGAR-D#1VKScq)AhxNr!++v(THO6af{bh28@qq6maul-`Rpr58gF zMFc5=gdPHfUP1{Z@g98M=e^$@_ue0z!5EO_?32CsT5GO3=bo0!Hr=(gSG<4*=o+fb z?=Iec$9aMLi>P}DZA<0z=C$W^wAm3-g1W*OMM|HsEa`g(_#WFUdlr5L+uGwH6X-`38CsM5>qGSm@!3RM zC*6(ojIe^aM+*6wPSuy{Y8Ck&Yo6z%y6lp3G|DyD^LGS5`REUj&T$X*LhgMuN3G28 zm5Y~Lx~UO&z>~eZL2N+Z2jDaTju1rgCLSZ@H(dYH146vX^u>t^InhlME3}I zot?bepzkPji^npH$AWtgwpx8LH}fnSHe-Q8mO8nddHP~o`dk7wruFd_6#reKdtlBz5&WZLbi zbR4W9;sidd#ZXH%ABkgM5cU`lYD3XySz=m`!!}2ch=7)DbDWYuJ%5(LH6m*+XhV^S z)Cv29UK4-?Ia5%(g(I((QoPZ(5MdZVszbHx|5l=pQl`EToRRbuQUD-z@fyEh35#>c z8*bt=G@wHt&!T@nn+5LxDS$2kM)viN(%>CUa`>5%G(*myTn!Q$PIx%Z%MdqiydQiS zR^1;Drb^0@P;Oa+>=s8#v5p7v9^F6`ZK_^0q1j=7?(rak1H34wsIOEkC3=`(15FG7JD_{OKCNJ#wT#RYf5=^#-e4e|Z2+sksWm{#fN@B2IM zv)p>yKn*I5vio!>Q?X_oh99KFFQx$5+7n?1n{oz~up_1VxT9^p`zoGQcX#nNH}+dqCHw7BaQpAM z$Bsl)o^48GkL-Q%XIBmP+<+8U;e5jd`cFX;O)<+p(3tK5pOrw8R$z!d34Lh%^3l_4 zi0s&(f6sJCONs=*H4xA6dAL-Hfs>B-&Uh#>6+|mSC zHgzpYRzS=DWf2jwBXmhBIPy1lWH;G2ko7{Pml)z*9C7#sQ{bf#+gK$(m%3g~*GU^% zzWkREk*mNbUpW%T91$o+0T^ovh7vAuboq7iC7!BJ{SUeyN~K1rf#Zh9jSVn8{!2zhsGcu3?c)0nHMc6SmHKS+;*b-VE6B2qqwrqy0$luA2-nW_${3#T|+59^KfF z4UvuuTAg&ZBK`$@V?lw*t9m~AvPbo%K$Rl~E5NBvs(*$qeBgTfrwOiTjt z?M6WX?$9JXHLyK*YUL%IKe_r{ltg0^-!e?3jakl3{OM;OewSA{qt9gaO)g##a^>A! zphbQ(MBjJkG*u4pu|FUs?b!_Vp*)M9A`%sRQ62%k>%MHfE*(D0oW2C-?|K#d-!YlauKt-e2@b>~BNkG|uEd^CNKM@yru3l2fA$gQg~ z#$0_52scb_#JL96bNQ%b=L9{Pl)DhPW+M=n&;pdSg!vpZo!MU-Y*86)Gi4&}cMZ5| zwG*Rv0#_X^DJUI9Jutsdd{Uf4<%Mya=HTArKvrHbgIN_K-FlbLiYsG^;BW-TgxfWV z@~_lq=-$IrSElo;canEAgXHhk!rBDY4-LcOxE!z@9ulbTJvF!yed5YP-KSXu=@ z*b4ig0KvFVUUx?0IFd-<4%xY>)OWQr2vWt%km@pij_N8=i=yP~$w!JYoITIOCx_?E5T&B@V0W;1T@P)Y zNvW9i_om~>dFmk1M3L7M3oh~+l;!`66ug3@1K5$z|x-ZEqEx0ucz{5%{X=nV~-ZMf*S>RHXS~mvVEnN_O$xaK=N_ zXzkR1v(1LcSg*du{8Nhv+5zYohCnD9;#zG^3K=^^lQis{gSasiB$X59GQDIQAZvrl z!Ik-M54wiS^fH>yZK6f zuIk=Xe!3al1zWp@8a(Or&Wcf=N`IU4jW0m+yz5sVA6kaQ5Qa%8@X!A2G-qe7{P=HV z3jT@z2bYygi3grz#mBi{jkUZYLXD?F(VXWN^O8)Ok{&z|ZbAw8S4JntBwkA%{iO7U zf1QmrO+z$y_s)M{s1y4Ckin@Fcg2MQDz!1`xAVf`DvJ7HN}4F8v9bzR_u z#|{LblS{{6kn8U+z=dIAP&X++0GO?bGGok;;*N1L+$@uZLsPBO;RWgX@gI<9m-A?j zF8O2S@+~?KnY8i9b7HbC71mC)}@)Khkg|A1!r zV83F2{aW1jsQwz!CrTcD?0GZ`=?H!#fmg`wtU7c|D`Oh3vH=z9u`Ka3<}x@&yVnfsJxQ*TRNE8|uI~PjH!3=k?mY8!6<@XXrs=7jX&I ziz-7)VMf-=Mi4SB>(gm7pu*GajR-U_jj$&>WTO~?5ZUQ4Z&VDJ zc-d0q!hryEOF)$L)p!M0lZNnOsJ$4iREdcklEl)%n5BlZUk4PPCp!MiEv?v!1Q42Va$G*)REO|M9~_+Jh@LFT=y_dP8YnMp#wYU@~vU%gJ*x;crp*nB3E^ zJI{U`Ve4G7TpKOH_G`jHbbI(Xzrq3BR2^R<#^eQ^RgltL%>$Xrrwp%)Ycs z?=)=(t@25oHV{{n$9`OzuFVGcV{=H&xt50kMSnnei9aBqT|o`|x?sm20mR12KcK9q zxqd$LH3s@`mz1!_IW;QPXKU+dxy=K?u)c8rZ?^QzyBrP!9<#BUdG6fyY;`FJPxCAA z>UwAl-bA)_-};+!9_Ely&QjG?;bYK~2a$QT5DgH;LuvSGcS82QZ1n!MEe^omkRR@C^;4^<`=aVz8b*52A<-jt2d(g0ycGK7-uuYr z_n11a78eDIl5uj?l;)b$%j;J6>12G0+ICvC+EOWsF2W20Np7$-rE8&4UYf%@c%q@( zV$?%H=sH&VZ5P>$8Ko0hS_#>2!y>JSo7HrgG3TB`efQMCY!W60qTZdp{( z)!hhwaGw^Fk#Sg|;p^dGqvTHW;EE{G?d+x3{Ipb9_Jw`B7Rs$Oi4QN#wsFXL&Mrzp zl+h9NzDa4tnd#?~)c%!8&$VVtg2K;RqrqoHv$ z?7Pmdz5VqVN??A;)r>;vq5@6&$4UOYcVuO--3e6-{rGf6vz z2JE&^dR6qhbiunyCJUaI+P=U1qA_Fpj(86qA-1+YKDEE}8Xi2zx&6?s^IC@VtDW*Y z`fd9hswwaxIdqghgbN%zx&ejVn4r28ADJeeckgMaX}n=3oZWcoXw!pb$-Zm zgzXQy2aZA>TvR*V6Ob`yT=+JjNB#O3_$HomvASt!FUG#OYZB|KIpzgPEONA8o`Z-+2_II z2(?E{hc-s;LG1;0PQ)v){a#JnnD>bwQFPEREhf96dRw{DB%s=F^ao)W0lnKS! zT3D>j7t4N`-6$tZq}TVg9B^)-OF0d|r9^^5qA?a_%A_OB}9e}}k} zHp_pA!~hTBuV5C4X=eZs&x-Zcnis@!GvJ+<;l(RH+F?TJ`stNdH&J!9 zjxKEmMk}IO}I9Z9t zPUWUwLua-)uk}?W46t*3?R4)cebFD-P`%##TM&^LL%hSsqsAfb3T6e%DLJ(+vBv$M zZ=LUa;ni!l@yDrYzlnx55+v#7bSN={LX9Hw6pdQi1^fYRG5VU=3;1Y16I=U@X!2rq z9mfiOlN-4E3{rqMpV^9kg*tgkCxf;}0E0mM9RyHxc8vbdA+rK~bP*uAnPoo@KK=p2 z0k03R0Di!&1hPnn z74ZU`{9}5r{A4tZ|9w#Tx))x(WoMgAE&J|Y1aMy)HO2ljnk&tgI7Z)ZVi9pj_Xo5P+W((HcE>|SxM9G|Th_=r zx#66+j)w6lUNOj(C7k6Z6J8(DrdtN^)&6^9|Mzq1PPNLm*~pH{K<*Vhf)J7qDLfN3 zi`!x~pXnyK@{EuP0TTEXVaF!z(x&a4$INHJ8|8=k|M%N0w#adMmYnM>j%#6@>3L=5 zo$(k*T>*Ew2H?)`PV@bWtbo1Xa})nDf)8HTO$_^ImE3C9{rj{rO=o8xNVT6I_s;3O zbcuIkk(uU=NJEbisv+cWKlB(@q^U%S?(q4M~=sAwT>Jqq|l9 z#@S>;^mMG>hwiZhg9kz_X0?zMR>V6GWku{#MS=u`L&(@9tUxF1+~wbK9Xe@dfjEEV z@&d~T$3EPb>C5S(mA{CLe4W(9z3~M1>Ea$)ZpE!;>~8&?;B<+W%&%U3y+oTlLLS{v zNg!}0|NG5FZM@9*yE0F+*VnRHg z%1Iuuw$cal=MabuTtK5~?n})n=-SW!J-z%#5eX-2kHJsdbX|giQ`uUZ!5SOSO-P;n zp(p{~V!Jssv6{}CICs8x;dyFy6l^m_|D0RLI88oZRGv)>S@?ghy??hG3EZ*NZASeu zoO8o%1k)aM&GquC4+x0vb7qUYkuq@iH{C>=9;*;R*P@kic0o*d;sd`YOn1~5{6o@h zDBIleA(p@V-#%~FyDh|K>?Dq8$WOZZ?_RNM{LhXio_H(dtm`bIZU(EF4AExE*w02U zy1cTaFkiRwKtT^S|9~p;ESXozT#{lX2F(2PW7Ydzpcx zv|9-lh^zGXk|3l3fP-R5iWW5ea^6fby6#gBkYPWP)-T%PZzcB)|CH5+|OiH`tM{3|Ic1zXg*@q95woyZR)nvJ@E+G z0Er$KBvU3Q__6yznSved{e#jAB`?GIgoTLFF1KcPfiaauzpV8p z6T(y2!QcWXct*E7p+vhh+r~e%IUbBI#6sWN%K_g_+J9me&28O};>KqVU@=v=zJg2TVWwRyx>0@zW*rf?-HPg*~a!X9Y2vg zVzcs|KJ(-$-IuG_zN@odNqwA&16FdJCTUA}nW56Xh@s8+y7&5*FI=4}CR&*}IIz-r zG1#yU4{S4f&&4@h-2P%61WFX~UN+}M+yJ_!+q92jjlq2%>h5r!>qbLz!<8~ce&oEO$rVMD9Q5J9%$i=`c^by0He%*#rqMHYb$&mF(PO`R$W)ez%QEb8Ag`t2Jdj>? z!4Bwn=n4sde=`@8?CMr|U>oJoV19(isY}pR+GAkL$qav6_T3TAvF>?QVkAhedrcWs z=szAd8H$*_Q+sHr3M7}sVau8nrI(i*oSKNH9HaD%T$hy58gTp6|vd zHshyq{Hb7kc)URE?(J9};Sw#5!TwG%Cr6-Ej3{ z4CdvS{?|}y6X0}+cK`N4ue~ylJZEDfqCGwCpn{3AHbZ&9>P+TInRaE2xdH$ZXjbXr zO0!S4{+UoXA`C~^FFY|B+(c%Fkh+T|Y-N@W_=#ra{fVpHgoCn>ex6HS%DHj`E*i5G z=f`(|{7oXtM}G!IT@sK#8(q8rRR{Vn%W_52XLGByIUU=@`=Y6SKzwgAvn@Qt+D~b) zSxGHnBA%5bU(Xg`NEwIkbIx|hxc)&yAi67X6!UP}I7JDH$U`gMpIzN6vxrWui@A?$Gon4~V-mw59WjvqWq zguV5p7^U`iS@?TGMIgKD0H; zGnV_-92m@^QlRvjaNJ2d`2b-09VP&rDg)|~nbl9H>I~qo`_WDsolxXXe!k>GiN<=6 z2f;BDQqX^ohgsw6k|#@!L_5|Q#Z=C=j(mArB&6Y4i%R{#tS@%?5_i>Gi#Hn&`M93k zh8#lVxKY8m*WM|8U*|4}qDAd;l11g6QUfXC#VJ3QpnktiQS(~4Neh8ti=ZrceQ(A_ z(VV6eLd1lt-?F-MU(d)^l!-jr?#?%(WT#oJzF(9}ya&Bcg)Go1zI4PF*~BwmTo4Wy%~Pz{t;dkZKdm1Ry?U3;YY(wNkS$P4>7(j< z_fn!T)>SvQWvy5I==$#1k2i(U)lH5sE4V{5Sp_$64j`Lgg(C`VoqNPg?8%1ak5zGLZOF@(be%Fql-z_8>U*L3pMnq0pA$5|oeqq+O zt$(YD!K=96c%6a_Wk-1ghnNfYuN>M8uz}!Oj03Ik?xPM>3SLhtx*5dh+&?2o09U)n zPwjG(C>|@{&R0eWZIrQg|90j>eRX^W%24|nazwZDMqXW|ooK48{IZ`Y0AN)qc+&$H zck3x2wLgLOFb_*|ryf1D(1d$1xkg1qalGqIUFf3SRD{>VqeLq|b@-~)Ch*mzq=i&v z@HdKCXg#`SJ)S(m%*YX?Vlh7F=-5Q0P3>opZ%yibt_Q5|x<2k;*R!QSCPS~1!O$-- zBwfkZUFlX5acpe4Ib@|3bn6*5~s4T@@m(SWQ>`94*D`$kTFv#|k7fKLybYbk3Rh|RA}kQVVQ+S@XTaT5ji{KD9^#)H@I-!XnF zW|N@kcn;uV#2}Zn_gz(vvw&Z_3c_Obit)g3J%ajT}0v`hmdxPU&6upV2+ELHA z)lX_^$veJ{+~(L`cH1VmN7)5P!nq=T0nr_o#Vo>;#ryeq0h4C6}G%db%mZ2 zg{KU7SY{hPs8hMQGkMd<(Q9IYai1f;&4LTRoW?#pDkU(^I4m za}q7YCN()jz+Sae6FC(yHLWxA0I>Az`Lrc2poxK7s%{Tmv93csFV$kMCpR@np^Zh* zg?h*J*4#$rn{Ng_xkMrq<&Uq330$>TSp|7_jhUtn^(GR-8{`{kUjdOf|8d`?-(otGTtB;-td4vckX@N>UB5M2CsB8c_O@-2e?hEMCd@h!*rY@~v9+VA6%D8)T=x$euC zl)d|R@90J49ssIj?<2a3APdc}8`r#-RqR1r%Hxbv5~}ATD(zWo6OM;!)d9HL_bBjn zsd@1vB1c#hm3#nAY4)l(E__)h92%dgh#I{k4>hFt92rRtyqjQ=zk!rMHAl6RV~)r| zY^wmMa9KD5Rp%4zb%KLjuGSC#(x(XD9dxUlE#j~mp2d-F~5Bfs&k4DNl zCNN7BUu2Vl1A&C-&gDtsh%sTJe0F`~9*C&}1f9ZVpJgTlA4g!1(If@BfZRxqL^+|L zdXoiCckF6-Kx4}g@r$Uis#&~}En}Usd?#4+v841H&4Wt-7raoUs}Dh3(If$U(DHj9 zo?!Hp!Yby}NcEXi`b)vgJoPc}S_n>>JDrbeYqiiH&D@XMGtrOzZfrwa+_X8fnGHOs zpg0N5R+!;BK>IOXH8=e@FN7n;+xXp>5^3hsQ*f1M2_jrqfK(h_c!Yvoi=EKQL{tOD zB(CVnYp-TZLs7&F3o%}4kzN`3DEJD>a1C#J{$zi#Y*W6-~9x&I$G6rcz30ENo= z6JF>EIg|y&YD&YA#Rj{(d^5#Fv-+ENyYlE~u{G`17!C9+JhIP>^{ebAf#ad3`NM@; zm^LT@LG_lj3|3q>qHl)}i>6~{(HCgdFsM3VG)pu4wUOnqPpz=7#kQ~hvW?gBOG?cBG>&O5#O z;j+fqo&4Zo1+hq$pOIZ z1ptRqlXKr=A0=a{C7`x!y|qOvKKZ9^QAaTR;6=~34+k4eN}s*DoYM`jb6#*;liM%m zWK6K~-*Vi9!nQ6%P&8Sn_`Mj0Xwu$Cka8JMLuz$kc_wzYPa|6`MAWFd@LK9B&ANle zMftbhOAGG?{Cc*qz$mvY2|Jn+qROWAv)bG_Ghjl&fQuhHpG=X?7`G1>h7ko*D1*#6Hxs(GtHhogUSrdBAjpe0_LZo zv8J;Zo&JE9hVtJ`=4?7uJv$^R*&8?Cc>M?s0RD*s{g)=p`iq;+fYskzo2;3coESqia^)Y=u1Xu{nt=PTY!kjnK>$pxUdfcP}8pEXxPtQ`ISlBFJylmiJ?ec5Tyqo*yDuzq#0zvSRG#LtiTPoOKIHWIX}j@PWxS0(IWd9IxbK(eQ;5Qhq=1IR}{sB zE1M!yCPg|_3^J{(_Vrgug(;fCOq~^q!FxwXwGN&~dJ-~3W``PFQig0Mc*GYHSoNg= zWcPI-7dg0a#MnY{?@GPLR?V(8wqXlYdzMScJQ?O+k&9Z&O<#Y8)dA)XEJ2ahMiCDH zO9>CZ+C#{W@Jgxc{o%CAe5xx}5Ow`k-B>DlSqbRWp8uY%%{qCeH;cF=dg?ar@j7>U z?jShf4sl>(4oMi;6(nuniq^~!>@8{&+9Uh{UC>~*zsM#R>6hX38NoOcDr6f|TS5NL zhclc}raC&vq-U6WYipInEIf%0uB||;YLO%!mLWMD;8jNkieD=|SM2dpylZoD$AHWP zf2c>rVBrv~=%Lc$k+-|B%TSq?LLhLRRt_~&bFhk`ym;S+@U|)H68EJG=^i-C?+^gC zAqELkDfB^qr=|1k*4-~N&78tS4Bc{s-{=KMG@lN`3V|~^5=!$g{tT|ANzO>~ji8zt zik+ZgUW>iM5o|!!11$E_3fN}wl^F+>HQ?z1@&qlG1)l|mir~w^iI|-r<$md#O#zqs zVe9@SL`H=E1ABqgH((T94tOV%Hpk(+Opco@M)bd`K zb;J?_M3cY1RkUTK{wj`phaCfBNJpm5+|rHwRnS#i@>)(AtO1qaI&bH(HfGs(M26^I zdmWK3f;gufh_DL)stXUvFyv<9s|PAm8o(J93!Ye}GFs>Pa_ME!!fk)bvnofm`5!{F zfNK=ZVlKW6ENj)7~;F#gwsV4N3CpFms5qZp5{54MgL%sZ{VA)69n@5mb%_= zsw>Kkl=yp0Z*Q}&592X}%rp4qPBw)-gEJ&y;4yskpy$CHm;e=n%Hda504dt*v;h&EM8sCPxMddZb3NBNFTH>E znL#XP?C%AM4aLieBbRI#RZM`U!`J2Maf-3D{6gj;^ADWYsfbDJ7^rq!h-$ZEx z&}!=20Z83UpVeVSx`@hE(zjUrZ;b8lAGx=kQ-sIE3OH*NA=`M9Q>C6CY(GS4CgzOg z!?;IJZA}UEng7VMGps6F({#Qcn4H;g=M&E{dFJQ_knP`HexR(scvtL3;@w{?^WzD8 zQo^M3$VG&jf$H*SyJm);^$UyfyQ8d zPp%+nw;qqJ#y#o|YQ26HR>aBhbCeFU&tOM1Rlnvzo zvi-%$CBLN}&Ln++TY3C6_`|-l2(0Q}2>Toi!#6zcMJi(bHqs_9g~~n;yP|oZ>EM2( zC-%)>a^nMR>UcimgQw%NE5S!j&qU7tvX zr$R1YP}iE}&{JH^+XPnD&!1ST!nye=7gl_2hDpLU!D6=41ujsjl7KZoPkB5AuF9l+ z{LVk8U4=d(;OD3s1(6`*JXPFEo;D|ADd6y0mW^l6njg%ao@WElRB$UNcA_}pt&Z`2 zr22!(1QY8E+p7Evki@pUaSYLWmj-y>`2vz8&hu|OV%iIXKA`B)0l_L&1;+;j4a6NArRJwvkwODrt|JX z_!sUSL|3-^y_F+R#Ecu5;RlMQ$mt9Y=^P=hT=7B@zs8)pKuP`Uc&wOli zFB-cPzipzjCsZEoJ-_7UdXOYJeea;aGaC6|Nsc5Hye4v4s4S=d#QO7OhiCh*sDRE9 z@N)h?qW};WPE^PMsqnO1@oBN(fp*yQU_gUK}ap5DV{@iyLxt%2XpULDk;AociiOfXVs9VUfv9UqQP1c9N zp*yseA9*4WmB3~^Ox>(~fv5vzN9(UBB71X+a^9WiHmp>J1DL407eFa3r(!0;{bzUT zHWUr_+aX8awZ`Xw20CEgDPo)SUiokbXgfD z5@}8Bj$bqG0!)|o_KPkqqm$>M^-3(tL=67hd&jhz;^-SO%cTh_bBo56674@f5#hldtFm zjm8`MzvouFOgcV-TJFvag`B;XeIr@EjU1p-{pv^5wA70Xap%1VSnM$)1$7s+d@6d? z<(yKt%GBjhZ6vh|EGWYX-RzcOCw}o&r*OskxcAfc+6pdwiG$I5ek;DN*>+SWH)X2C zQEYt=XYQ!|=F+{LPB@9*QyZ3pi0gZSuM_h>kcnhfMNW!QWAsWhw* z2@k~tR;aqPmsuDoEyhU+PNP>M9PRL_poIhFkifJe=k@2q8Eb}Fnw+&czK+TxCLZpc zMWUrhu{?RhxHqF8mz6Yd9cIL@5$+|Y;b(UEsG?n_+M$*Xo<-TInvf$^fn-Mww#lNX z-)p{*p`t1QQ{VH;Crb4d;b^nB6T+YTZ4>kpE$3U-l-<^cic@TNl_nFc@mP+SIA@DFvVViiImf2RZpMR4Frw8CpvWpAH z*k;J=w$%;vqRON&6PZ&EzBz)&1+eeNA3hYB+A7u-^G3*>vfU-WM5VDG59SAa$=RW0 z0i{2Nwu(U-qG9)e1SLUMLp+H`vl31gQT~P6?f(PX%mQduL(Vl^e9oP9WKbsk@Lg>0 zl>5v|(#D3$x5k&5=esdB%sS|D>x}lRpWOtS9-N|s*F#Lesn~SdQ%2W@f@^$gs7Pl= z{H$GQYjlJe(%DteB8_W7kfuvX6B-iht^3lkH-0AIj9YBbTIWZf4!`$SoQ&MU3tJX8rtm zu`$ijG3QiF+8a^{5*vR6xr=w=uZ=oA>@cHz`QaJRcd|SIT!B9tm1(Z64jRX=*o43o zYnku1H~LcHNiYjhi8Qb?@~}>Hm_VvKf;?K%_qZHC8v7fOL3AfI(Cu}W}YN9HqYmoTXU$&$^TGE<}LCLH)N|GR&0f)T=@gak8=yzC za8`_eap9NF1ih7Ywq;ymc>Ff=>FZrZIH zI4`0F3R6{Gf5=)}fGGI=AZIWB+5EU~12SELa>Nk4f_iky`b){V&s4y{G55vWmfX5> zj}ln6)IjA|2zkZlSM;>5L`e*N(gDS?FvJbgsp?8DT@Y&P8K_W3&ykAWU+6i|1Q#vS zVom14rTd#(IXO@V@6Q`Y1?U7M~HWsXNq4id=N=C zb@OEVfq>-k!!IpFhr`|xjv$8XI+CFrJRC-Qu#UjG_4WJohMLYV-JqLZ0n;lvTPteV zp`ym72#edX_FG?~t)fFs_!cR6E=n4*b|@QwmmYnOEBnT?sq{gP>bC zEy_9t$CI_hhulRpL4H8JD*}qNF$+qx&bagA?9maObNX?6`mL=?H8UI7p+xVQ_d5@5 z7bI3QZf}S*gLFl%D9#Tg`aUn*hw0*OsqgpJ#BI(79yiUj9H>oE3ZSmRp2vHM;=y^Zjc^8p}C zZl@d!dg1kZ&ZAS9rqc#)L9(C=MvW80)RMC4=20QXVMl}FvS%bN=RXAulQTB`pbK&Z z?>Z#d=ZPN}KpO0tWiVA=19jHs)~OCZc)jjzg7Oy^5UKdAK++E}*9bZEgPe*ZD%Hz1 zzIEpCf)zM9eRXzK*I;NHxkNi@-2|J2z5uLedsw02*FX z=ZU5pDh+vDBvbBKP!8)hI6{jh7^7~WNNcdQG1JGK@uZq03V4h%wWx7HC$8Qvo|wGw z#RVaj(VM@+V*d1!#x92vskGXglUrd#*7H&#|A%giYX~0~m`X;egm#8D`i z_$&0=r_z_V516!;{W4-C4(H&6qEsx6k0ReoX%Uko0k)cB0m>aefkayKde5~hL1;nR z*Wxh7>PI&^mWyyB)59K7pO`0} zzpmsOs@U^Eya8yn?HGB)S<5ZUv8*X_1|4iO4!85DZ{yL8PrBVt@#2q|t~TKoz`VaC zWo^hc&}*;OiN1zn9-<%ewc^fqLi_2|cX)#*eIcew=`ALj=h8Y|7_}F=H~{v#v+tAM zf@|_1*?}JtBwYf9g;hVNTzYC|wwZ~wavGcS7Re90L=hbO`OxRG#+ly?+W~9mx=+g- zv0C0H(U0%E%(Si@p@Di~A{dtjZHo9u_}m@%Cym1f(mzXzLyp%68QZF^^C0ya#=l?x zlo)#l{8WUY5wbzFZ&`ax;QUnpN*em4EZ}&^Jahg#?N%%MEaW*H{j>UHqps0c_nJp4m7ix5{(OM(7;YXH0o zaMbu)2%XUW|2feZe4=3c=g@<{LF9zcbrL^L&O$vYc>d$6aZ>dFYJsd1pTUzqov-GX zy5}sS>xAKt4B=z!^kL;$z&q?FTH^c|L6Ftf;u$mbxqxY168I4=?M!3P2H@ev_{B4)s8?Uo2c zZ}I4S;G`^~&qW?O10E)49gSWFB|u+{ld@J|g@7_(&nR6>H*H|!n2BW3espB`^i4yG z`McpFimKmmXHRQnJlg<=A_Ks`Ar77r!~M`@o243rsOd%-gxe_Pcvuz`WaY*(hx2ph zxIl(00o7_H@495qfoUdX8$gFHgTCVE%|g-BGnCIqelg=mkgh_b`FS_%G^9&HgKI*bZqjuAsWS%;az)jq`7OlB_E$k(i>6?q}@_3Nn~tb*}$3TiUOjpGyPpG6-8aSejz$A)##p5apT!Z)Rc7$Fkwg% zS~R=7f&56?Q8}^#LL_~j-(IQC)({sIB-`uyEv?~T{)3Ifu&88}U*JFwX}c3@*fDqNhY{PSZPAbY z)wT5|_B=~3W!TwV;KAL62Q$JGUa5dP;35QXrlc&h>D;K>O%Ps8E>yprzP(1q9VnK! zOqe6jX`i6ipbK47F&g}CfUh0mNC=+ zJ)^$w|6N_K=HkrEnRm{4-sicW`?>GmFcs=FHM@&L{@=GRlk^!&hFH&rj{OQ>?#cc_ zF{FYsy>xQfB7w%3J?T1_&!M19s32|>+UI!u+OCza9>62lzFPK9=RAE!@e)$EBl?io zCfiO=U1|Nsl2Fiz#qA`+Z7;adHn!Qw_l<|}EWcsCy0lC83(w_|Xj(qr^HE1UJ>WOr z@3fiUH+$VDXc6|a9yvt0xyAjf%|kRfMgF@ooct0H+Tpp{HWO5Foz$%<5k)1yib(D2 zO%Zu;q3^0nd5)_Wap03*c2##&N9QjQFe*{%tI{LI=H!Z{Ue&twQ*XZ%XTGcs9Pdi( zzk=n;a#&;{4s@lk+4}jNk3N0pFXDaHmq3%YZ!7zpC5piQxB zVguC*kSJaOBZOxCcSiY7E8~CdYyNgHEE)PN{#P)T4rX;hvtKwc-MlL~&bXg_P)}!q zzu^kD=4teh`s->GfvG;7OTa0hUY6qgmH*s}`n={zg@qg3a8&e+Fh}~Z!C0yhtX$V? zRxM4`ctHzZg>Gj0dK?~pY{O}M-SM)_-MFGBeK+DhvB7}Y3J0vx_prA;nAK5-&*wWm z(dc<;hYW;|3UdC2bw@Q&s50tDQChDfuP^15uanpKHm?nx$RKiQk0ZD#$G4-cI4g9S z`6ca~QBd{Q_z9{Vb?T}xmAJk$%Wo6R9t#{QONOWqw#*41v{z58h_tM4Tf&qd zn;$Be9n#!R)uDY}-M03sk~qlWs4Wy=*%uD<9Ifqfdjiav}Hi}7{m!mOz{*CmUY4B0Do|{c(y6@C&RI30tnfl z6h9m{5lXt81fN+j*;9UVRq5U5??cLU8}N?D503Un_|@G?Zs_BlS}&gbu$O!#jHVUT zK8@s_GGnxgx4fo-sS)Lv(~ z#~a7b4;BP1*t~=1PZc`~-d9dKrIqnuoji+_foav{Q!N*e!_i1_-~;<)ar)ON1- z7eeN6jaScB*#V_NgATGpBM=v0_$+s4Ts%2(!BO9va6Gl$lDRui@+KPIvVvKeSi>Va zOnw?-EbFEuejro8YkjQ67iv(g;FF0c^kGt%a^1zGG(I<9p>-yamM$w1qxEwb;>$s- zoWJFkj6CX`*&^0|L%)uxL5CbdTf=S)f(#ah=6_6*+|nNs{zmCsGQZ)+=j_JN4|+m} zGc?s{pJin{G+Fi7iHPlBeuw#wb8spdSmAV5?E6JP88f~74Vx@jb0U|*o;@x4d8Rt{ z+K)AIk-)5cbV#zlC{dlpOYQO!zB3V@mhbNL1HEa%X!x)?-UBs{a@DpbqMsp+T&vv{ z;)RK$a#*pLP3OK1G-GC#czcj#4mZ9-kCn|Ul`7erKTHHH#S@_LB9#;{d_7(FGEF=s zJtdTEPMk7G$X`12=B&nc_G(46VOVHN8x?6%ne(a@*vW|(qf^BfpBi(dI3Xl z*>|3w?fP=zObhv@t-W2mV*9@v8g#0UDImBB6_Y0L21~+*qKDvIK0uH{doUkTWwh#S zVR;~jvYbD~hpS{Bmxu^HN^0D0**QxlEiKG^%@R$YG#EoG8{TlRcGSoN0TNzeV*PJt zYY$8U{VF#FiQh;aUn51_T)TgAq}9pk_y>(JMINGXF41&0?|K)DylE)rY>#)pmfDZ( zw4i&K79^!0vSeZXY4|B24&a?gcd4OgpVFMbw+7g7nd>tBujU&_SJNPj1SSdWYFI!t zWCNBqf9w@OOmr2vbDu7LZH5E$2GTePeFxpz-N?q+4bs=s0J(-#fS~?u3}E`mGpSb3 zP1c<73wUB|j1%xAI%;TyYg~2eoDrrb-HU8ib2;k!`g>ONe0p9IGF>>8U$18Vgs3OB zYF%8nOl|Uta+f;oANVlxT?0`f9hKCYR43o9Z`qH{H;VY!ARu|fJR)NIUQLNEqMX=2 zX?E*S2aCFmW%pZ}=tyINx6M^0E~vqKLi!+1599?29N0IYE(hvuR=;!x^|(zeFB_@d z(2Ja?4QP}Q!C`Br{Ws7q-)dkeA(FH%@$QfkZ5sS*%(5vcIa3+WzF>&jOO^SAsbkW_ zq;WMcVPmK=HFh_FB&px{xPDm_f(51?Q+r0RqoakBol+VB{18fOE`R0~x-j`-C&~_c zgS=BJE$-RmD~Q%}%tu$$D2kVnd#HKHf8xjU)5BdF>$UUmcs=lRyAqqUu?DBQm}Y$; z{h-`xP6diw_OP(nfF<0LFmai?7>r+47(vR~vZwc8LQWnu-cTQEAl2%h*~ApVY_pX_ z3Df!t$icN87G0qV_};E{%XVa(!7n47yV1*mohR0X^(W0_vc)V*L^)vHmVV@b7A395 z;~UwYbBhR>2WiHKhFWB)K(kcISX0*UI;EJAyP$X?j3*)UN(3g=3wAeF`1!m$aubY} z0;+tLEprKHJx^DPlCAHtn??Q0Wy}l=PPg&GbySu!lTD)m|GxR8w4H)9f>{PE(dzeT z7qYD$Dk)Ig%s}U9i2)&FzwG{`=zrJ-E+Vh6fks2@HrFL1)!>gW4LWh5Gu7Dj=|g!Y z9*ai#DYjdQ+V9mTtrX|aC?Iof+86ZLiWpBA`kjiGM<;*-dY)G1V_Imx8k=#6EN4Fz9G#5d@Z&k?O*;Td)C^8K3pS!WIy z1+jId&X^a~SJ0j*`%hQnUI@wl_)snrWOKyO?^W0sw^0I>ANyuBBXw@4G$Irken4NB zivy$nNgp19Se~&4`|52<&u`d7?Djp_^A{>8mRB;>KJQq6wuJNE?z58LyDZu3(>@)B z?IcVj-Wdt#iFyFLB{gteXtj6r8KP8xpafqle7<4ci3RGbFmv%TpXxJttt+FrY3ZT^ z3Kunp*3W}VA{1;njQV~<4hmhv33C3k*p@_{$+vxMkwG;6ojSk=gC~=G72Ztgn&S+F zar20ooDr?}r;@IeBGYJ3GGp_rha*W_Ob`8j!GBm&LM3vz%1YhrANChFN@pePyg=*1 zTSuD_+oDHnlI6uNcx_o%`Cr~6WYD>D7W)~15z7;8n159X2kKWV!;VVkq*|cE?~zw7 zQ(^-dcx?q@vIo;;ffbsr+ z{az)-@_~s92M9Ng58nY@NeEZfg=qkt7f68j(r)D0jXpr@1w4=ktU`7Wbnb#TGCAGT zd&!f*6BN%E8;ZWvlP8S(KK1KG=HNO}L{h!SX7;ks7uinDTYEk>v@<+E#1F-c%r%(^ z_+(G%*P7g&V?0l0UGl!ETQtszV!jlTk*&GhzTwxDJ?I>##nScEoR%YTWvBe!Yc=x8blQc^wPVY)-MEb%sP-5MyqIuBQ@JIL2`=nq+ z4Tfn=4tJQ{fp~MFEtx_NeQ?#xMMPT-NGRofXvhf%W=vMra)GL-6b_Nn(O}#G)e%x) zOkX}qBfmLcHo-5=#mdKg9$8UtpTTdvQvTv4?&6sZ+&rts_E=zV;^zVuJW)a~pkY&@ z61aBF8l)MFJH!bQx~>-NC1F-7;fuf+2^r$y%98@RA3lF4v5<1x?}?Mji^;Vl{dr`4strb%|?K^~hamv&aZ4SbK^VXmjf`7Lo& z6%<3Fz;BqfDDiQ=^vkMI;7$0)=SvYh8-}8v%9d6J` zTbTmdBPCA~#l;6?Kmp@a&#{5DcLu{Vsa$8cPZz@5Ey8}o);dm5zNa3=ezt2|Dx~q{ z835VyO6;iq4(QDQs(2vx;-l%&w;+y0u4vEd;VapdWiDZ^pJ7+wT<}dapb#5$ZkfOJ z@Vjcp_5=7OH+*b)Ke_a7-Te^ksz}q}M=Pp#m6iPdB_i*5eF%KZcT2ntuuvmDQ$P_~&8XAjF5C0y}MqlndxzT}(c5 z83rjOr7Due-uvpc7LnJGCRpAe^QNgHW72@%lLH3){0H6q@p}$)B^LH>^*A~XId%RV z;#mPukJ=;A>EedWaBCwl(wGr*%TQO6kq(}LtfV<3(Fg?yV<_-|4v+zm%m}s%uRVUu z0Fbx;s1(Lyfqe(-os7hFE+tTe?K;X=R}cPmg8JXFNgv=X3NeyrfrB5Qm+_NxEr6yj0D*81^Dj82wy`wy+r=sBhvlSg?6(k(X zojqfhg{b-1mN|FT^QSa#;faT{2zJEV?=d$ykA}(bW7X9ydC>jPSY#W@DH|XDZZCeeHOxUy;q#H`h57_4kZ+Jnt=vL; z+xWEi$-7hJ$M2m?4&L(pxKc9D>R&xU=m<~gW32k8Uf;#XC9=$Y&$T_g0kW!3H@V*5 z;e~aWj^u|3B@%Hg9~P4IyTS=B)aYx49(mtU?jc#9STEcnJnwYt{VC?pvETYsJ8+_- zveh8Pcm!NY@2Av1H`txS+`g5Drd&=QO~qJW^5juD-SN6FL1#E3O5e-BHF%7>;rMZC zcke2)BL{|&8YCg?f|<(E@*U^f-Ex)k;uP5d{DYoEb!1wN==eH|qg^%{m-2rVU#nff zMGm|}P^EY<)C(2$csvDg!@kk14;af~cq*BC36nj!{xJ$~dldI$o3Ptk#h#2E)dA1- zQaOn*B5|gts4nGL&UFjU6YWg<>Q9JFz>{@+TJt=1R#_Sq@}J2tv@so5Ta_9nNK`uN zk0&18!Xzpru-w>wz}?Bu?wSygi|_N_DP3s$FuwD|rz5ef1}A}2MrtryJbJR)N8L=L zvW`;O_`=fQ4FbqvS7e?iBS&Q*jUbYE7wA*y9-9L?z@r=ekori(o!V7MaSlBJ2f6}X z%E|oN6qg>g9jDZ8-Q&4%PatL?f_&nSifN~)x{)t3&Ag>mwn=2KBeRWQH=nslc)9+wrUSH2zUdarAN$~L&_ZDv zB?)`NFQ!&30WFNi?^sSNKiPJ*D}3Ni-6>|YO_2?jKE5Oz+BA;U2J92QW{=l77_$pQ z3un!T$ky<7o8;PDg+d>4l?A(t*wH*wUJ`u7mt>oldkSw@qT_MI!84|ilcS;2UnNzS zxLuO6D1u^mOhjDVjW^`d!HGFS!-U&H*@=DIc&681&1}()~jdn_Ewc<1L>&u&^-V>v( z1FJm*iN0B`nq_t7h1bFg^(h@08qy0LC9q|pM3L=G8{{t^!y?INrU(h}nCnW4La@9G z2s3m}Dgb;yzCuTUEI_pcOy%gny}c1DU#7VMH!wnA1%SQT1fZlQLTuE#*PsVi0k0J@ z2LhwmAm4x>B1SNeN0+37*@Kt@{hH2#1*i|0$pbjQm?xky9UQDFU$?ar=%6Wucr^x} zJmm>;fXvvRX#6l)`0}wt*tBcUqg;`xv-cMyZEqZ6dFfy-FiyBrn%muVtyTWplV ziZy~z(r_PEw>*@Wojh6#vX>N7tt`<#-Z0Q^N1ca={(pH zEaz=2c^@_w%#@Owtg>K1*#1}*?rY}1R^VoIe*iO67q_#C|3sMz5pqvHSDSINE=koe zw`n;GegeTj^osphi+%9UR%_fLweK*qjxPGuH%clnCgi3aOa9t81gye=6D|V_I`BhG zf}XX+Of~n6-kj4SlDY=OX7a+E8BYwwQi^6j&PK8GaTa#Dtv%Gi)~T%h#2=Kqu4;Ig zXT!ieee>Ss^Ft@k9Z5ZtI+@69f9-2zy#?%k*F@OfjuoU_a4AW?FS18~tV1trtior@9)CCik$c_VgiDE-7`>VLiL8JZiJw3Dpvu&k;qB**h}4dvs@{s4 z1G-7_qE7Wx9_&b9+@ZBkQ7jkif*i!|=lr|VxcaGo;d<1?cXrr1BoB(Oe2voiy3pg2 z;62^B>MLv|2tG`n*VZk=UhFsJSzdCAvhFKDPoH2#Z`kf7A!ch9%flkrt2JF}&-)N= zcqd_==r3cNUToU9iz+yd4$mHt%l3Qi+$_al&h|)ZT)%g@GzfwKGU3lwg6?D z5$wS94E{g6F1VGzokK?qEa+~y5JAF7>hWW6qX9R@Uiyt=G64h$q;X)My#&b}!0hdz ztrwI9dyW77fAYc5T|%$gAyG%@4x%6JOy89O!T{~ciLB7m*il*{L$nLBUi%`wG8hHH-q*p772()?+g@?|L_x67&44^$%|B_vj}at7xm38I<-2zbz4MOe<#x7rGY~8Yvkru>M~8Qv|<>pb3wi$K2nb$<$%uQiXcu$iIj2g z-OL7oIJJ`x*-I4Jt|C7-xAX{(}mXPAj5cxM173&Il^d|5mBs7&E3~ zdN23Sj?qfrY<878r1n?;X%zqnO_vGKj0zZ2z}zRG66r%yW9@9R+tjG4Jjtg(HR76z(ih`R(92Bg3Mw*!llffM+XV$)f>e-;P2 zmH+13e_Ptp^!0#&6x$Ol3_UAnfPh6PQh?O_@ng2%Up8s`sYBIZA+OOtAfE*|0=f*OL316dC=#Lb2j9o;@$@q;U;!v8z082p1iIZH$PUEf zz$yVW6v_$`SwX*NlLit2lOKSl%(1Jt0?-Ll`ugpDud_ZHyU7EvpM}O%uG3%XPm3Q! zFerZZSb%r*q+grRXO=Di$3fr)c%$7P=&W_1=luDBVkz|bV3096ILLqzz$pxd#=0=I x-OpzR4QfKy`k%wVp_!T0e|*aSa@wHlPEu0izJVwGOOn2{PxJtIy2tN<{{ssKv*G{% literal 0 HcmV?d00001 diff --git a/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/vanilla.ts b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/vanilla.ts new file mode 100644 index 000000000..4f87ed08b --- /dev/null +++ b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/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 From 0549195cbe86583a93d52b89d5700b35a2925d44 Mon Sep 17 00:00:00 2001 From: antichaosdb Date: Thu, 26 Mar 2026 21:52:12 +0000 Subject: [PATCH 2/2] docs(smith-chart): add research notes and redesign spec for arc-based axis implementation Co-Authored-By: Claude Sonnet 4.6 --- .../SmithChart/SMITH_CHART_REDESIGN_SPEC.md | 313 ++++++++++++++++++ .../SmithChart/SMITH_CHART_RESEARCH.md | 185 +++++++++++ 2 files changed, 498 insertions(+) create mode 100644 Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/SMITH_CHART_REDESIGN_SPEC.md create mode 100644 Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/SMITH_CHART_RESEARCH.md diff --git a/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/SMITH_CHART_REDESIGN_SPEC.md b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/SMITH_CHART_REDESIGN_SPEC.md new file mode 100644 index 000000000..2e32819b1 --- /dev/null +++ b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/SMITH_CHART_REDESIGN_SPEC.md @@ -0,0 +1,313 @@ +# Smith Chart Redesign Spec + +## Goal + +Replace the current `drawExample.ts` implementation (which draws ~40 circles/arcs as 200-point `XyDataSeries` polylines) with a new architecture using paired custom axis classes that draw true hardware-accelerated arcs via `renderContext.drawArcs`. This is SciChart-idiomatic, eliminates ~40 series objects and ~8000 data points, and produces geometrically perfect circles regardless of zoom level. + +--- + +## Architecture + +Split into two files: + +### `smithChartAxes.ts` (new) + +Exports: + +- `SmithResistanceTickProvider` — custom tick provider for R values +- `SmithReactanceTickProvider` — custom tick provider for X values +- `SmithChartResistanceAxis extends NumericAxis` — X axis; draws constant-R circles + unit circle + real axis +- `SmithChartReactanceAxis extends NumericAxis` — Y axis; draws constant-X arcs (± mirrored) + +### `drawExample.ts` (rewrite) + +- Wire `SmithChartResistanceAxis` as xAxis, `SmithChartReactanceAxis` as yAxis +- Replace 3× `XyDataSeries` highlight series with 3× `ArcAnnotation` +- Keep `SmithChartDragModifier` (unchanged logic) +- Remove `preRender` aspect-ratio hack (no longer needed — `getArcParams` takes `aspectRatio`) +- Keep all `TextAnnotation` readouts + +--- + +## Smith Chart Mathematics + +### Coordinate system + +The Γ plane is a standard Cartesian plane (Γ_real on X axis, Γ_imag on Y axis). The unit circle is the boundary |Γ| = 1. Data lives within the unit disk. + +### Constant-R circles (drawn by `SmithChartResistanceAxis`) + +For normalised resistance `r`: + +- Centre: `(r/(1+r), 0)` in data coordinates +- Radius: `1/(1+r)` in data coordinates +- All circles pass through `(1, 0)` and are tangent to the right edge + +### Constant-X arcs (drawn by `SmithChartReactanceAxis`) + +For normalised reactance `x` (both `+x` and `-x` are mirrored): + +- Centre: `(1, 1/x)` in data coordinates (or `(1, -1/x)` for negative) +- Radius: `1/|x|` in data coordinates +- Arc runs from `(1, 0)` (right edge of chart) to unit-circle intersection: + - `xInt = (x²-1)/(1+x²)`, `yInt = ±2x/(1+x²)` + +--- + +## The `drawGridLines` Override Pattern + +Both axes follow the pattern from `PolarAxisBase.ts`: + +```ts +protected drawGridLines(renderContext, tickCoords, pen, isMajor) { + // 1. Get the sibling axis via this.parentSurface + // 2. Get the viewport pixel dimensions from coordinate calculators + // 3. Get raw data values (R or X) from this.getTicksWithCoords() + // (ignore the tickCoords parameter — it gives pixel positions but we need data values) + // 4. For each data value, compute circle/arc centre and radius in data coords + // 5. Convert to pixel coords using xCalc.getCoordinate() / yCalc.getCoordinate() + // 6. Compute aspectRatio = xCalc.getCoordWidth(1) / yCalc.getCoordWidth(1) + // 7. Call getArcParams(...) to create SCRTArcVertex + // 8. Push into vecArcs, call renderContext.drawArcs(...) +} +``` + +### Axis cross-wiring + +Like `PolarAxisBase`, in `measure()` each axis locates its sibling: + +```ts +// In SmithChartResistanceAxis (X axis): +const yAxis = this.parentSurface.yAxes.get(0) as SmithChartReactanceAxis; +this.sibling = yAxis; + +// In SmithChartReactanceAxis (Y axis): +const xAxis = this.parentSurface.xAxes.get(0) as SmithChartResistanceAxis; +this.sibling = xAxis; +``` + +Both axes need to know the sibling exists to draw grid lines that span the full viewport. + +--- + +## Key SciChart APIs + +### `getArcParams` (from `NativeObject.ts`) + +```ts +getArcParams( + wasmContext: TSciChart, + centerX: number, // pixel X of arc centre (in native/viewport coords) + centerY: number, // pixel Y of arc centre (FLIPPED: vpHeight - yCalc.getCoordinate(cy_data)) + startAngle: number, // radians; 0 = full circle start + endAngle: number, // radians; 2π = full circle + radius: number, // pixel radius + innerRadius: number,// 0 for lines + isGridLineMode: number, // 1 = anti-aliased arc line, 0 = filled sector + aspectRatio: number, // xCalc.getCoordWidth(1) / yCalc.getCoordWidth(1) + lineThickness: number +): SCRTArcParams +``` + +### `renderContext.drawArcs` (from `WebGlRenderContext2D.ts`) + +```ts +renderContext.drawArcs( + vertices: VectorArcVertex, + xCenter: number, // pixel X of viewport reference centre (for rotation) + yCenter: number, // pixel Y (native, flipped) + rotation: number, // 0 for Smith chart + clipRect: Rect, + pen: SCRTPen, + brush?: SCRTBrush, + left: number, // svr.left (series view rect left) + top: number // svr.top +) +``` + +### `getVectorArcVertex` / `getArcVertex` + +```ts +import { getVectorArcVertex, getArcVertex } from "scichart/Charting/Visuals/..."; +// or from the same NativeObject imports as getArcParams +const vecArcs = getVectorArcVertex(wasmContext); +const arc = getArcVertex(wasmContext); +arc.MakeCircularArc(arcParms); +vecArcs.push_back(arc); +``` + +### WebGL Y-flip + +In SciChart's native pixel space, Y is flipped: `nativeY = vpHeight - pixelY` + +- `vpHeight = yCalc.getCoordinate(yAxis.visibleRange.min)` (bottom of viewport) +- Or compute from `renderContext` viewport dimensions + +--- + +## `ArcAnnotation` for Interactive Highlights + +Used for the 3 interactive highlight overlays (R circle, X arc, |Γ| circle). + +### Full circle (for R highlight and |Γ| highlight) + +```ts +// Circle centred at (cx, 0) with radius rad: +new ArcAnnotation({ + x1: cx - rad, + y1: 0, // left point on diameter + x2: cx + rad, + y2: 0, // right point on diameter + height: 0, // chord IS the diameter → height = 0 gives full circle + isLineMode: true, + stroke: "#FF4444", + strokeThickness: 2.5, +}); +``` + +**Update on drag:** set `.x1`, `.x2`, `.height` properties directly — no series rebuild. + +### Arc segment (for X highlight) + +For constant-X arc with circle centre `(cx, cy)`, start point `(1, 0)`, end point `(xInt, yInt)`: + +```ts +const height = arcHeightFromCenter(1, 0, xInt, yInt, cx, cy); +new ArcAnnotation({ + x1: 1, + y1: 0, + x2: xInt, + y2: yInt, + height, + isLineMode: true, + stroke: "#4488FF", + strokeThickness: 2.5, +}); +``` + +### `arcHeightFromCenter` helper + +Given two endpoints and the circle centre, compute the `height` parameter (perpendicular offset from chord midpoint to arc): + +```ts +function arcHeightFromCenter(x1: number, y1: number, x2: number, y2: number, cx: number, cy: number): number { + const midX = (x1 + x2) / 2; + const midY = (y1 + y2) / 2; + const diffX = x2 - x1; + const diffY = y2 - y1; + const len = Math.sqrt(diffX * diffX + diffY * diffY); + // Normal vector pointing from chord midpoint toward circle centre + const normalX = diffY / len; + const normalY = -diffX / len; + // Project (centre - midpoint) onto normal + return (cx - midX) * normalX + (cy - midY) * normalY; +} +``` + +--- + +## Tick Values + +### `SmithResistanceTickProvider` + +```ts +majorTicks = [0, 0.2, 0.5, 1, 2, 5, 10, 20, 50]; +minorTicks = [0.1, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9, 1.2, 1.4, 1.6, 1.8, 3, 4]; +``` + +### `SmithReactanceTickProvider` + +```ts +majorTicks = [0.2, 0.5, 1, 2, 5, 10, 20, 50]; // positive only; negative mirror is implicit +minorTicks = [0.1, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9, 1.2, 1.4, 1.6, 1.8, 3, 4]; +``` + +--- + +## Ownership of Grid Elements + +| Element | Drawn by | +| --------------------------- | ------------------------------------------------------------------------------ | +| Unit circle | `SmithChartResistanceAxis.drawGridLines` (r=0 case or always drawn separately) | +| Real axis line (horizontal) | `SmithChartResistanceAxis.drawGridLines` | +| Constant-R circles | `SmithChartResistanceAxis.drawGridLines` | +| Constant-X arcs (+X) | `SmithChartReactanceAxis.drawGridLines` | +| Constant-X arcs (−X) | `SmithChartReactanceAxis.drawGridLines` (mirrored) | +| R labels | `TextAnnotation` in `drawExample.ts` (unchanged) | +| X labels | `TextAnnotation` in `drawExample.ts` (unchanged) | +| Interactive highlights | 3× `ArcAnnotation` in `drawExample.ts` | +| Draggable point | `XyScatterRenderableSeries` in `drawExample.ts` (unchanged) | +| Readout text | `TextAnnotation` in `drawExample.ts` (unchanged) | + +--- + +## What Gets Removed vs Kept + +### Removed from `drawExample.ts` + +- `createCircle()` helper function +- `createReactanceArc()` helper function +- `createLine()` helper function +- All `FastLineRenderableSeries` for grid circles/arcs (~40 series) +- All `XyDataSeries` for grid circles/arcs +- `preserveAspectRatio()` function +- `preRender.subscribe()` aspect-ratio hack +- `rCircleDS`, `xArcDS`, `gammaCircleDS` (`XyDataSeries`) +- 3× `FastLineRenderableSeries` for highlights +- `populateRCircle()`, `populateXArc()`, `populateCircle()` functions + +### Added to `drawExample.ts` + +- Import of `SmithChartResistanceAxis`, `SmithChartReactanceAxis` from `./smithChartAxes` +- Import of `ArcAnnotation` from scichart +- `arcHeightFromCenter()` helper +- `rCircleAnnotation`, `xArcAnnotation`, `gammaCircleAnnotation` — 3× `ArcAnnotation` +- `updateInteractiveElements()` sets `.x1/.x2/.height` on `ArcAnnotation` objects instead of rebuilding `XyDataSeries` + +### Kept in `drawExample.ts` (unchanged) + +- `SciChartSurface.create()` +- All `TextAnnotation` readouts +- `SmithChartDragModifier` class +- `pointDS` / `XyScatterRenderableSeries` (draggable dot) +- `ZoomExtentsModifier`, `MouseWheelZoomModifier`, `PinchZoomModifier` +- Impedance conversion formula: `Z = (1+Γ)/(1−Γ)` + +--- + +## Files to Create/Modify + +| File | Action | +| ------------------- | ------------------------------------------------------------------------ | +| `smithChartAxes.ts` | **Create** — tick providers + axis subclasses | +| `drawExample.ts` | **Rewrite** — wire new axes, ArcAnnotation highlights, remove old series | + +No other files need to change (index.tsx, exampleInfo.tsx, vanilla.ts are untouched). + +--- + +## Open Questions / Risks + +1. **Import paths for `getArcParams`, `getVectorArcVertex`, `getArcVertex`** — need to confirm exact import path from scichart package (likely `scichart/Charting/Visuals/RenderContext/NativeObject` or similar). Look at PolarAxisBase.ts imports. + +2. **`drawGridLines` override signature** — confirm exact TypeScript signature from `AxisBase2D.ts`. It likely takes `(renderContext: WebGlRenderContext2D, tickCoords: number[], linesPen: SCRTPen, isMajor: boolean)`. + +3. **`getTicksWithCoords()` method** — confirm this method exists on `AxisBase2D` and returns `{ majorTicks, minorTicks, majorTickCoords, minorTickCoords }` with the raw data values accessible. + +4. **`ArcAnnotation` height=0 for full circle** — needs verification. The chord `(cx-rad, 0)` to `(cx+rad, 0)` is a diameter, so `height=0` should give a semicircle — may need `height` small positive/negative, or two arcs. **Alternative:** Use `x1=cx, y1=rad, x2=cx, y2=-rad` (vertical chord) to avoid ambiguity. + +5. **Clip rectangle for `drawArcs`** — the `clipRect` parameter clips arcs to the unit circle. Determine whether to pass the series view rect or a custom unit-circle clip. + +6. **`measure()` override** — confirm that `measure()` is the right lifecycle hook for setting `this.sibling`. It may need to be `onAttach()` or similar. + +--- + +## Reference Files to Read Before Implementing + +These files are in the SciChart source (likely under `C:\SciChart\SciChart.Dev.Alt\` or the npm package): + +- `Charting/Visuals/Axis/PolarAxisBase.ts` — the key pattern to follow +- `Charting/Annotations/ArcAnnotation.ts` — full circle / arc annotation API +- `Charting/Visuals/Axis/AxisBase2D.ts` — `drawGridLines` signature, `getTicksWithCoords` +- `Charting/Visuals/RenderContext/NativeObject.ts` — `getArcParams`, `getVectorArcVertex`, `getArcVertex` +- `Charting/Visuals/RenderContext/WebGlRenderContext2D.ts` — `drawArcs` signature diff --git a/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/SMITH_CHART_RESEARCH.md b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/SMITH_CHART_RESEARCH.md new file mode 100644 index 000000000..660a0f8b0 --- /dev/null +++ b/Examples/src/components/Examples/Charts2D/ModifyAxisBehavior/SmithChart/SMITH_CHART_RESEARCH.md @@ -0,0 +1,185 @@ +# Smith Chart Research: Engineering Use Cases & Feature Roadmap + +## What Problem the Smith Chart Solves + +The Smith chart is a graphical calculator for RF/microwave engineers working with **complex impedance**. +At RF frequencies, components have complex impedances that vary with frequency, and mismatched impedances +cause reflections that waste power and damage equipment. Engineers need to: + +1. Visualise how impedance changes along transmission lines +2. Design networks that transform one impedance to another (matching) +3. Trade off competing objectives (gain vs. noise vs. stability) in amplifier design + +The chart works by mapping the complex impedance plane `z = R + jX` through a Möbius transformation +onto the unit disk, where position = reflection coefficient Γ = (Z_L − Z_0) / (Z_L + Z_0). + +--- + +## Key Curve Families + +| Family | Shape | Centre / Radius | +| --------------------------- | -------------------------------------- | -------------------------------------------- | +| Constant resistance circles | Circles tangent to right edge at (1,0) | Centre (r/(1+r), 0), radius 1/(1+r) | +| Constant reactance arcs | Arcs tangent to right edge | Centre (1, 1/x), radius 1/\|x\| | +| Constant \|Γ\| circles | Concentric circles at origin | = constant VSWR = constant return loss | +| Constant angle lines | Radial spokes from centre | Phase angle of Γ | +| Q circles | Circles through origin | Constant Q = \|x\|/r — used in filter design | + +--- + +## Outer Rim Scales + +A traditional paper Smith chart has three concentric scales around the outer rim: + +1. **Wavelengths Toward Generator (WTG)** — 0 to 0.5λ, running clockwise. + Moving clockwise by a value here = moving that electrical distance toward the source along a transmission line. +2. **Wavelengths Toward Load (WTL)** — 0 to 0.5λ, counter-clockwise. +3. **Angle of reflection coefficient** — degrees (±180°). The phase of Γ at that point. + +The WTG/WTL scales are critical: a lossless transmission line of length `d` rotates the impedance +point **clockwise** by `2d/λ` turns (0.5λ = one full revolution = back to start). + +--- + +## Component Movements on the Chart + +This is the heart of matching network design. Each component type traces a specific arc: + +| Component added | Movement on Z-chart | +| ------------------------------------- | ------------------------------------------------------------------- | +| Series inductor (+jωL) | Moves **up** along constant-R circle (increasing reactance) | +| Series capacitor (−j/ωC) | Moves **down** along constant-R circle | +| Series resistor (+R) | Moves **right** along constant-X arc | +| Shunt inductor | Moves along constant-G circle (easier on Y-chart) | +| Shunt capacitor | Moves along constant-G circle (easier on Y-chart) | +| Shunt resistor | Moves along constant-B arc (easier on Y-chart) | +| Lossless transmission line (length d) | Rotates **clockwise** around origin at constant \|Γ\| by 2d/λ turns | +| Lossy transmission line | Spirals inward clockwise toward centre | + +**Key rule:** Series components are easiest to work with on the Z (impedance) chart; +shunt/parallel components are easiest on the Y (admittance) chart. +Switching between Z and Y is a 180° rotation about the centre. + +--- + +## Core Engineering Workflows + +### 1. Transmission Line Analysis + +- Plot the load impedance point Z_L +- The impedance seen at distance `d` from the load = rotate clockwise by `2d/λ` on a constant-|Γ| circle +- Read the resulting Z, VSWR, return loss at any distance using the WTG outer scale + +### 2. Single-Stub Matching + +- From the load point, rotate along a constant-|Γ| circle until hitting the unit conductance circle (g=1 on Y-chart) +- Add a shunt stub (open or short-circuit stub) of the right length to cancel the susceptance → reach centre (Γ=0) + +### 3. L-Network Matching + +- Goal: move the load impedance to the chart centre (Γ=0, Z=Z₀, perfect match) +- Choose a series component to move along a constant-R circle until hitting a desired constant-|Γ| circle +- Then choose a shunt component to arc into the centre +- (Or shunt first, then series — two solutions exist) + +### 4. Amplifier Design (Advanced) + +- Plot **input stability circle** and **output stability circle** from transistor S-parameters +- Plot **available gain circles** (constant transducer gain G_T, concentric but not centred at origin) +- Plot **noise figure circles** (constant F, centred at Γ_opt) +- Find the source impedance Γ_S that trades off gain vs. noise +- Design input/output matching networks to hit those targets + +--- + +## Key Readouts Needed at Any Point + +When an engineer places a marker or hovers over a point, they want to see: + +| Value | Formula / Source | +| ---------------------------- | ----------------------------------------------- | +| Γ (complex) | x + jy directly from position | +| \|Γ\| | Distance from chart centre | +| ∠Γ (degrees) | Angle from positive real axis | +| Normalised impedance z | r + jx (read from resistance/reactance circles) | +| Normalised admittance y | g + jb (rotate 180° or read Y overlay) | +| VSWR | (1 + \|Γ\|) / (1 − \|Γ\|) | +| Return loss | −20 log₁₀(\|Γ\|) dB | +| Mismatch loss | −10 log₁₀(1 − \|Γ\|²) dB | +| Wavelengths toward generator | WTG outer scale | +| Wavelengths toward load | WTL outer scale | +| Q factor | \|x\| / r | + +--- + +## Advanced Overlays + +These circles are drawn on top of the base chart for specific use cases: + +- **Stability circles** — for amplifier design; circles bounding stable source/load regions. + Can be centred anywhere, including outside the unit circle. +- **Available gain circles** — loci of constant transducer gain G_T. Concentric circles whose + centre moves toward the optimal Γ_opt. +- **Noise figure circles** — loci of constant noise figure F. Centre at Γ_opt (optimal noise + match), expanding outward as F increases. +- **VSWR target circle** — user-defined VSWR limit (e.g. VSWR=2.0); the region inside this + circle represents "acceptable" match. Often shaded. +- **Frequency locus** — the path traced by a load as frequency sweeps. Shows how impedance + changes with frequency; often a curved arc or spiral. + +--- + +## Features of Existing Interactive Smith Chart Tools + +Tools like SimSmith, Smith V3 (web), Keysight ADS, and RF Tools provide: + +- **Click/drag** to place or move an impedance point +- **Component palette** — click "add series L", "add shunt C", "add TL section"; the arc from + current point to new point is drawn and the new position shown +- **Frequency sweep** — input component values and see the full frequency locus over a band +- **Z / Y / ZY toggle** — switch between impedance grid, admittance grid, or combined "immittance" view +- **Multiple numbered markers** — pin multiple points, display full readout table for each +- **WTG/WTL cursor** — snap to outer scale, show current electrical position on transmission line +- **VSWR goal circle** — shade the region inside a target VSWR circle +- **Stability / gain / noise circles** — input transistor S-parameters (Touchstone .s2p) and draw design circles +- **Touchstone file import** — read a measured .s1p or .s2p file and plot the frequency locus directly + +--- + +## Proposed Feature Roadmap for SciChart Smith Chart + +Ordered by engineering value: + +### Tier 1 — Core Usefulness + +1. **Full readout panel** — show |Γ|, ∠Γ, Z (r+jx), Y (g+jb), VSWR, return loss, mismatch loss, Q, WTG, WTL simultaneously when hovering or at a placed marker +2. **Outer rim scales** — WTG, WTL, and angle of Γ rendered as tick marks / labels around the perimeter +3. **Multiple named markers** — pin points on the chart, label them (M1, M2…), compare values + +### Tier 2 — Matching Design + +4. **Admittance (Y) overlay** — toggle a second grid rotated 180° (constant-G circles, constant-B arcs drawn in a different colour) +5. **VSWR target circle** — user sets a VSWR limit (e.g. 2.0), draw and optionally shade that circle +6. **Component trace** — select a component type (series L/C/R, shunt L/C/R, TL section), enter its value, draw the arc from the current point to the new impedance point + +### Tier 3 — RF Engineering / VNA Data + +7. **Frequency locus** — import a Touchstone .s1p file and plot impedance vs. frequency as a trace on the chart; markers snap to specific frequencies +8. **Frequency sweep animation** — animate the point moving along the frequency locus + +### Tier 4 — Amplifier Design (Advanced) + +9. **Stability circles** — input S-parameters, compute and draw source/load stability circles +10. **Available gain circles** — compute and draw constant-G_T circles +11. **Noise figure circles** — compute and draw constant-F circles + +--- + +## Note on Coordinate System (Cartesian vs Polar) + +The current Cartesian SciChartSurface implementation is the correct approach. +The Γ plane IS a Cartesian plane (real/imaginary axes). The Smith chart's "polar feel" +comes from data living within the unit disk, not from the gridlines being polar curves. +The constant-R circles and constant-X arcs are offset circles (not centred at the origin) +and do not simplify in polar coordinates — they would become more complex curves. +The Cartesian approach used here matches how every professional Smith chart renderer works.