diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 4c7f8e2..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,72 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added -- Comprehensive MkDocs documentation -- GitHub Actions workflow for automatic docs deployment -- pyproject.toml for modern Python packaging - -### Changed -- Migrated from setup.py to pyproject.toml - -## [0.3.1] - 2024-12-11 - -### Added -- Automatic Pandas index handling (`x='index'` or automatic detection) -- `geom_range` for 5-year historical range plots -- `geom_searoute` for maritime shipping routes -- `geom_edgebundle` for network visualization -- `geom_candlestick` and `geom_ohlc` for financial charts -- `geom_point_3d`, `geom_surface`, `geom_wireframe` for 3D plots -- `scale_x_rangeslider` and `scale_x_rangeselector` for interactive time series -- `coord_sf` for map projections -- 15 built-in datasets (mpg, diamonds, iris, mtcars, economics, etc.) -- `guide_legend` and `guide_colorbar` for legend customization -- Facet labellers (`label_both`, `label_value`) - -### Changed -- Improved faceting with consistent colors across panels - -## [0.3.0] - 2024-11-01 - -### Added -- `geom_contour` and `geom_contour_filled` -- `geom_jitter` and `geom_rug` -- `geom_abline` for diagonal reference lines -- `position_dodge`, `position_stack`, `position_fill`, `position_nudge` -- `stat_summary` and `stat_ecdf` -- `scale_color_gradient` and `scale_fill_viridis_c` -- `theme_bbc` and `theme_nytimes` - -## [0.2.0] - 2024-09-01 - -### Added -- `geom_map` and `geom_sf` for geographic data -- `coord_polar` for pie charts -- `scale_x_date` and `scale_x_datetime` -- `scale_color_brewer` and `scale_fill_brewer` -- `annotate` function for text and shape annotations - -## [0.1.0] - 2024-08-01 - -### Added -- Initial release -- Core ggplot grammar: `ggplot`, `aes`, `+` operator -- Basic geoms: point, line, bar, histogram, boxplot, violin, area, ribbon -- Scales: continuous, log10, manual colors -- Themes: minimal, classic, dark, ggplot2 -- Faceting: `facet_wrap`, `facet_grid` -- Labels: `labs`, `ggtitle` -- Utilities: `ggsave`, `ggsize` - -[Unreleased]: https://github.com/bbcho/ggplotly/compare/v0.3.1...HEAD -[0.3.1]: https://github.com/bbcho/ggplotly/compare/v0.3.0...v0.3.1 -[0.3.0]: https://github.com/bbcho/ggplotly/compare/v0.2.0...v0.3.0 -[0.2.0]: https://github.com/bbcho/ggplotly/compare/v0.1.0...v0.2.0 -[0.1.0]: https://github.com/bbcho/ggplotly/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..90458f5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,636 @@ +# CLAUDE.md - Project Context for AI Assistants + +## Development Philosophy + +**IMPORTANT**: This library aims to faithfully replicate R's ggplot2 API in Python. + +When contributing or modifying code: +1. **Follow ggplot2 conventions** - Match R's ggplot2 function names, parameter names, and behavior as closely as possible +2. **Consult ggplot2 documentation** - When implementing existing ggplot2 features, reference https://ggplot2.tidyverse.org/reference/ +3. **Extrapolate for new features** - For functionality not in ggplot2 (e.g., `geom_candlestick`, `geom_stl`, `geom_sankey`), follow ggplot2 naming conventions and design patterns: + - Use `geom_*` prefix for geometric objects + - Use `stat_*` prefix for statistical transformations + - Use `scale_*_*` pattern for scales (e.g., `scale_x_log10`, `scale_color_manual`) + - Accept `aes()` mappings consistently + - Support `data=` parameter override in geoms +4. **Pythonic adaptations** - Only deviate from ggplot2 when Python requires it (e.g., strings for column names in `aes()`) + +## Project Overview + +**GGPLOTLY** is a Python data visualization library that combines R's ggplot2 Grammar of Graphics with Plotly's interactive capabilities. + +- Version: 0.3.5 (Beta) +- Author: Ben Cho +- Python: 3.9+ +- License: MIT + +## Quick Start + +```python +from ggplotly import ggplot, aes, geom_point, theme_minimal + +(ggplot(df, aes(x='col1', y='col2', color='category')) + + geom_point() + + theme_minimal()) +``` + +## Project Structure + +``` +ggplotly/ +├── ggplotly/ # Main package +│ ├── ggplot.py # Core ggplot class +│ ├── aes.py # Aesthetic mappings +│ ├── layer.py # Layer abstraction +│ ├── geoms/ # 44+ geometric objects +│ ├── stats/ # 13 statistical transformations +│ ├── scales/ # 17+ scales +│ ├── coords/ # Coordinate systems +│ ├── themes.py # 9 built-in themes +│ ├── facets.py # facet_wrap, facet_grid +│ └── data/ # Built-in datasets (CSV) +├── pytest/ # Test suite (39 files) +├── examples/ # Jupyter notebooks (43) +└── docs/ # MkDocs documentation +``` + +## Key Files + +- `ggplotly/ggplot.py` - Main ggplot class, rendering pipeline +- `ggplotly/layer.py` - Layer abstraction combining data, geom, stat, position +- `ggplotly/aes.py` - `aes()` function and `after_stat()` for aesthetic mappings +- `ggplotly/trace_builders.py` - Strategy pattern for Plotly trace creation +- `ggplotly/aesthetic_mapper.py` - Maps aesthetics to visual properties +- `ggplotly/data_utils.py` - Data normalization, index handling + +## Common Commands + +```bash +# Install +pip install -e . + +# Run tests +pytest pytest/ -v + +# Run specific test file +pytest pytest/test_geoms.py -v + +# Run example notebooks as tests (catches real-world usage bugs) +pytest --nbmake examples/*.ipynb + +# Run all tests including notebooks +pytest pytest/ -v && pytest --nbmake examples/*.ipynb + +# Build docs +mkdocs build + +# Serve docs locally +mkdocs serve +``` + +## Dependencies + +Core: pandas, plotly, numpy, scikit-learn, scipy, statsmodels + +Optional: +- `pip install ggplotly[geo]` - geopandas, shapely +- `pip install ggplotly[network]` - igraph, searoute + +## Architecture Notes + +1. **Grammar of Graphics**: Uses `+` operator for composition +2. **Immutable**: `+` returns copy, doesn't modify in place +3. **Strategy Pattern**: `trace_builders.py` handles different grouping scenarios +4. **Registry Pattern**: `ScaleRegistry` enforces one scale per aesthetic + +## Component Counts + +| Component | Count | Location | +|-----------|-------|----------| +| Geoms | 44+ | `ggplotly/geoms/` | +| Stats | 13 | `ggplotly/stats/` | +| Scales | 17+ | `ggplotly/scales/` | +| Themes | 9 | `ggplotly/themes.py` | +| Coords | 4 | `ggplotly/coords/` | +| Datasets | 16 | `ggplotly/data/` | + +## Testing Patterns + +Tests are in `pytest/` using pytest framework: +- `test_geoms.py` - Geom functionality +- `test_showcase.py` - Integration/showcase tests +- `test_stats_positions_limits.py` - Stats and positions +- `test_facets.py` - Faceting +- `test_scales.py` - Scale functionality + +## Testing Requirements + +**When adding new features or modifying existing code, tests MUST include all four categories:** + +### 1. Basic Functionality Tests +Verify the feature works as expected in isolation: +```python +def test_stroke_with_value(self): + """Test stroke parameter sets marker border width.""" + df = pd.DataFrame({"x": [1, 2], "y": [1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(stroke=2) + fig = plot.draw() + assert fig.data[0].marker.line.width == 2 +``` + +### 2. Edge Case Tests +Test boundary conditions, empty data, type variations: +```python +def test_stroke_with_large_value(self): + """Test stroke with unusually large value.""" + # ... + +def test_stroke_empty_dataframe(self): + """Test stroke with empty DataFrame doesn't crash.""" + df = pd.DataFrame({"x": [], "y": []}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(stroke=2) + fig = plot.draw() # Should not raise + +def test_stroke_with_float_value(self): + """Test stroke accepts float values.""" + # ... +``` + +### 3. Integration Tests (Faceting & Color Mappings) +Test with faceting, color aesthetics, and multiple geoms: +```python +def test_stroke_with_facet_wrap(self): + """Test stroke parameter works with faceting.""" + df = pd.DataFrame({ + "x": [1, 2, 3, 4], "y": [1, 2, 3, 4], + "cat": ["A", "A", "B", "B"] + }) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(stroke=2) + facet_wrap("cat") + fig = plot.draw() + # Verify stroke applied across all facets + for trace in fig.data: + if hasattr(trace, "marker") and trace.marker: + assert trace.marker.line.width == 2 + +def test_stroke_with_color_aesthetic(self): + """Test stroke works when color aesthetic is mapped.""" + df = pd.DataFrame({ + "x": [1, 2, 3], "y": [1, 2, 3], + "cat": ["A", "B", "C"] + }) + plot = ggplot(df, aes(x="x", y="y", color="cat")) + geom_point(stroke=1.5) + fig = plot.draw() + # Each category trace should have stroke + for trace in fig.data: + assert trace.marker.line.width == 1.5 +``` + +### 4. Visual Regression Tests +Capture and verify figure structure/properties: +```python +class TestVisualRegression: + """Visual regression tests that verify figure structure.""" + + def get_figure_signature(self, fig): + """Extract key properties from figure for comparison.""" + signature = {"num_traces": len(fig.data), "traces": []} + for trace in fig.data: + trace_sig = {"type": trace.type, "mode": getattr(trace, "mode", None)} + if hasattr(trace, "marker") and trace.marker: + trace_sig["marker"] = { + "size": getattr(trace.marker, "size", None), + "line_width": getattr(trace.marker.line, "width", None) if trace.marker.line else None, + } + signature["traces"].append(trace_sig) + return signature + + def test_stroke_visual_signature(self): + """Test that stroke produces expected visual signature.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(stroke=2.5) + fig = plot.draw() + sig = self.get_figure_signature(fig) + assert sig["num_traces"] == 1 + assert sig["traces"][0]["type"] == "scatter" + assert sig["traces"][0]["marker"]["line_width"] == 2.5 +``` + +### Test Naming Convention +- `test__default` - Test default behavior +- `test__with_value` - Test with explicit value +- `test__with_` - Test with specific aesthetic +- `test__empty_dataframe` - Test empty data handling +- `test__with_facet_wrap` - Test with faceting +- `test__visual_signature` - Visual regression test + +### Reference Test File +See `pytest/test_new_parameters.py` for comprehensive examples of all four test categories. + +## Available Aesthetics + +```python +aes( + x='column', # X-axis mapping + y='column', # Y-axis mapping + color='column', # Line/point color (categorical or continuous) + fill='column', # Fill color (bars, areas) + size='column', # Point/line size + shape='column', # Point shape (categorical) + alpha=0.5, # Transparency (0-1) + group='column', # Grouping without color + label='column', # Text labels +) +``` + +Use `after_stat()` to reference computed statistics: +```python +aes(y=after_stat('density')) # Use density instead of count in histograms +aes(y=after_stat('count / count.sum()')) # Proportions +``` + +## Common Geoms Reference + +| Geom | Use Case | +|------|----------| +| `geom_point()` | Scatter plots | +| `geom_line()` | Line charts | +| `geom_bar()` | Bar charts (stat='count' default) | +| `geom_col()` | Bar charts (stat='identity') | +| `geom_histogram()` | Histograms | +| `geom_boxplot()` | Box plots | +| `geom_violin()` | Violin plots | +| `geom_density()` | Density curves | +| `geom_smooth()` | Trend lines with CI | +| `geom_area()` | Area charts | +| `geom_tile()` | Heatmaps | +| `geom_text()` | Text labels | +| `geom_errorbar()` | Error bars | +| `geom_vline()`, `geom_hline()` | Reference lines | +| `geom_candlestick()` | Financial OHLC | +| `geom_map()` | Choropleth maps | + +## Built-in Datasets + +```python +from ggplotly import diamonds, mpg, iris, mtcars, economics, msleep, faithfuld +``` + +Available: diamonds, mpg, iris, mtcars, economics, msleep, faithfuld, seals, txhousing, midwest, and more in `ggplotly/data/` + +## Themes + +```python +theme_default() # Default Plotly theme +theme_minimal() # Clean, minimal +theme_classic() # Classic ggplot2 style +theme_dark() # Dark background +theme_ggplot2() # R ggplot2 style +theme_bw() # Black and white +theme_nytimes() # NYT style +theme_bbc() # BBC News style +``` + +## Position Adjustments + +```python +position_dodge() # Side by side (grouped bars) +position_jitter() # Add random noise (overlapping points) +position_stack() # Stack on top of each other +position_fill() # Stack normalized to 100% +position_nudge() # Shift by fixed amount +``` + +## Coordinate Systems + +```python +coord_cartesian(xlim=(0, 10)) # Zoom without clipping data +coord_flip() # Swap x and y axes +coord_polar() # Polar coordinates (pie charts) +coord_sf() # Geographic projections +``` + +## Index Handling + +Pandas index is automatically available: +```python +# Series: index becomes x-axis automatically +ggplot(series, aes(y='value')) # x uses index + +# DataFrame: reference index with 'index' +ggplot(df, aes(x='index', y='col')) + +# Named index becomes axis label automatically +``` + +## Faceting + +```python +# Wrap into rows/columns +facet_wrap('category', ncol=3) + +# Grid by two variables +facet_grid(rows='var1', cols='var2') + +# Free scales +facet_wrap('category', scales='free') # 'free_x', 'free_y' +``` + +## Scales + +```python +# Axis transforms +scale_x_log10() # Log scale +scale_x_continuous(limits=(0,100)) # Set range +scale_x_date(date_labels='%Y-%m') # Date formatting + +# Manual colors +scale_color_manual(['red', 'blue', 'green']) +scale_fill_manual({'A': 'red', 'B': 'blue'}) # Dict mapping + +# Color gradients +scale_color_gradient(low='white', high='red') +scale_fill_viridis_c() # Viridis colorscale + +# ColorBrewer palettes +scale_color_brewer(palette='Set1') +scale_fill_brewer(palette='Blues') + +# Interactive +scale_x_rangeslider() # Add range slider +scale_x_rangeselector() # Add range buttons +``` + +## Stats Reference + +| Stat | Purpose | Used By | +|------|---------|---------| +| `stat_identity` | No transformation | `geom_col`, `geom_point` | +| `stat_count` | Count observations | `geom_bar` | +| `stat_bin` | Bin data | `geom_histogram` | +| `stat_density` | Kernel density | `geom_density` | +| `stat_smooth` | Smoothed line + CI | `geom_smooth` | +| `stat_boxplot` | Boxplot stats | `geom_boxplot` | +| `stat_ecdf` | Empirical CDF | - | +| `stat_summary` | Summary statistics | - | +| `stat_function` | Apply function | - | +| `stat_qq` | Q-Q plot points | `geom_qq` | + +## Specialized Features + +### Financial Charts +```python +# Candlestick (requires open, high, low, close columns) +geom_candlestick(aes(x='date', open='open', high='high', low='low', close='close')) +geom_ohlc() # OHLC bars +geom_waterfall() # Waterfall charts +``` + +### 3D Plots +```python +geom_point_3d(aes(x='x', y='y', z='z')) +geom_surface() # 3D surface +geom_wireframe() # Wireframe surface +``` + +### Geographic (requires geopandas) +```python +geom_map(aes(fill='value')) # Choropleth +geom_sf() # Simple features +coord_sf(projection='...') # Map projections +``` + +### Network (requires igraph) +```python +geom_edgebundle() # Edge bundling +geom_sankey() # Sankey diagrams +``` + +## Adding New Features + +### New Geom +1. Create `ggplotly/geoms/geom_newname.py` +2. Inherit from `GeomBase` in `geom_base.py` +3. Implement `_draw_impl()` method +4. Export in `ggplotly/__init__.py` +5. Add tests in `pytest/test_geoms.py` + +### New Stat +1. Create `ggplotly/stats/stat_newname.py` +2. Inherit from `Stat` in `stat_base.py` +3. Implement `compute()` method returning `(data, mapping)` tuple +4. Export in `ggplotly/__init__.py` + +### New Scale +1. Create `ggplotly/scales/scale_newname.py` +2. Inherit from `Scale` in `scale_base.py` +3. Implement `apply()` method +4. Export in `ggplotly/__init__.py` + +## Geom Implementation Patterns + +### Default Parameters + +Geom parameters follow a three-level inheritance pattern: + +```python +class geom_example(Geom): + # Subclass defaults - override base class defaults + default_params = {"size": 2, "alpha": 0.8} +``` + +Parameter resolution order (later takes precedence): +1. **Base class defaults** (always applied): `{"na_rm": False, "show_legend": True}` +2. **Subclass `default_params`**: Class-specific defaults like `{"size": 2}` +3. **User-provided params**: Explicit values passed to constructor + +**Important**: Do NOT include `na_rm` or `show_legend` in subclass `default_params` - they are automatically inherited from the base class. + +#### Parameter Aliases + +The base class handles these ggplot2 compatibility aliases: +- `linewidth` → `size` (ggplot2 3.4+ compatibility) +- `colour` → `color` (British spelling) +- `showlegend` → `show_legend` (Plotly convention) + +Explicit user params take precedence: if both `linewidth=10` and `size=5` are passed, `size=5` wins. + +### The `before_add()` Hook + +The `before_add()` method is called when a geom is added to a plot via the `+` operator. Use it to: +- Create sub-layers (e.g., `geom_ribbon` creates multiple `geom_line` layers) +- Transform the geom before it's added to the plot +- Return additional layers to be added + +```python +class geom_ribbon(Geom): + def before_add(self): + # Create additional layers for ribbon edges + color = self.params.get("color", None) + + # Create line layers for ymin and ymax edges + min_line = geom_line(mapping=aes(x=self.mapping['x'], y=self.mapping['ymin']), + color=color) + max_line = geom_line(mapping=aes(x=self.mapping['x'], y=self.mapping['ymax']), + color=color) + + # Return list of additional layers + return [min_line, max_line] +``` + +**When to use `before_add()`:** +- Composite geoms that consist of multiple sub-geoms +- Geoms that need to generate additional visual elements +- When the geom itself shouldn't render but spawns other geoms + +**Implementation notes:** +- Return `None` (or omit return) if no additional layers needed +- Returned layers are added to the plot after the original geom +- The method is called by `ggplot.__add__()` during composition + +## Tips & Gotchas + +1. **String column names**: Always use strings in `aes()`: `aes(x='col')` not `aes(x=col)` + +2. **Parentheses for chaining**: Wrap in `()` for multi-line `+` chains: + ```python + (ggplot(df, aes(x='x', y='y')) + + geom_point() + + theme_minimal()) + ``` + +3. **geom_bar vs geom_col**: + - `geom_bar()` counts rows (stat='count') + - `geom_col()` uses y values directly (stat='identity') + +4. **Color vs Fill**: + - `color` = outline/line color + - `fill` = interior color (bars, areas, boxes) + +5. **Saving plots**: + ```python + from ggplotly import ggsave + ggsave(plot, 'output.html') # Interactive HTML + ggsave(plot, 'output.png') # Static image (requires kaleido) + ``` + +6. **Plot sizing**: + ```python + from ggplotly import ggsize + plot + ggsize(width=800, height=600) + ``` + +7. **Labels and titles**: + ```python + from ggplotly import labs + plot + labs(title='Title', x='X Label', y='Y Label', color='Legend') + ``` + +8. **Multiple geoms**: Layer geoms for complex plots: + ```python + (ggplot(df, aes(x='x', y='y')) + + geom_point() + + geom_smooth() + + geom_hline(yintercept=0)) + ``` + +9. **Access Plotly figure**: Get underlying figure for custom modifications: + ```python + fig = plot.draw() # Returns plotly.graph_objects.Figure + fig.update_layout(...) # Standard Plotly customization + ``` + +10. **Per-geom data**: Override plot data for specific geoms: + ```python + (ggplot(df1, aes(x='x', y='y')) + + geom_point() + + geom_line(data=df2)) # Different data for this geom + ``` + +## Key Example Notebooks + +Located in `examples/`: +- `ggplotly_master_examples.ipynb` - Comprehensive examples +- `Examples.ipynb` - Core functionality +- `view_all.ipynb` - Gallery of all features +- `prices.ipynb` - Financial data +- `maps.ipynb` - Geographic mapping +- `EdgeBundling.ipynb` - Network visualization + +## Differences from R's ggplot2 + +| ggplot2 (R) | ggplotly (Python) | +|-------------|-------------------| +| `aes(x = col)` | `aes(x='col')` (strings required) | +| `%+%` for data replacement | Not supported | +| `stat_bin(geom="line")` | Use `geom_line(stat=stat_bin)` | +| `theme(text = element_text(...))` | `theme(text=element_text(...))` | +| Automatic printing | Use `.show()` or Jupyter auto-display | + +## Debugging + +```python +# Check what data a geom receives +plot = ggplot(df, aes(x='x', y='y')) + geom_point() +fig = plot.draw() # Renders and returns figure + +# Inspect Plotly traces +for trace in fig.data: + print(trace) + +# Check aesthetic mappings +print(plot.mapping) # Shows aes mappings + +# Verify data normalization +from ggplotly.data_utils import normalize_data +normalized_df, mapping = normalize_data(df, aes(x='x', y='y')) +``` + +## Code Auditing Best Practices + +**Always test before flagging issues.** When auditing code for bugs or missing features: + +1. **Don't grep-and-flag** - Pattern matching on code structure without understanding behavior leads to false positives + +2. **Run the code** - A 30-second `python3 -c "..."` test catches most false positives: + ```python + # Instead of assuming geom_bar fill is broken because it's commented out: + python3 -c " + from ggplotly import ggplot, aes, geom_bar + import pandas as pd + df = pd.DataFrame({'x': ['A', 'B', 'C']}) + fig = (ggplot(df, aes(x='x')) + geom_bar(fill='red')).draw() + print(fig.data[0].marker.color) # Actually works! + " + ``` + +3. **Trace cross-file interactions** - Code in one file may have fallback logic in another (e.g., `geom_bar` relies on `geom_base._apply_color_targets` fallback) + +4. **Ask "why" before flagging** - If something looks wrong, investigate whether it's intentional design (e.g., `stat_edgebundle` doesn't inherit from `Stat` because it has a different API contract) + +5. **Check existing tests** - If tests pass for a "broken" feature, the feature probably works + +6. **Test diverse input types** - Don't just test the happy path. Test edge cases like: + - Dict input vs DataFrame vs Series + - Empty data, single row, large data + - Different column types (numeric, categorical, datetime) + - Missing values, NaN handling + + Example: The test suite only used DataFrames, so dict input to `ggplot()` was broken and went undetected. + +7. **Visually verify visualization code** - For charts and plots, passing tests and running without errors is NOT sufficient. The visual output IS the test: + - Generate the actual chart and **open it in a browser** to view it + - Check that visual properties match expectations (stacking, colors, positions, aspect ratios) + - Don't assume correct data flow means correct rendering + - **NEVER claim "all checks passed" without actually viewing the output** + + Examples of bugs only visible through actual visual inspection: + - Histogram code passed all tests and set `barmode='stack'`, but bars weren't actually stacking because each group had different bin edges + - `coord_fixed(ratio=2)` set the correct Plotly properties (`scaleratio=2`), but the `constrain='domain'` setting was overriding the aspect ratio - squares appeared as squares instead of tall rectangles + - Structural tests passed (correct number of traces, correct types), but visual output was completely wrong + + **Required visual verification process:** + ```python + fig = plot.draw() + fig.write_html('/tmp/test_output.html') + # THEN: open /tmp/test_output.html # Actually view the file! + ``` diff --git a/README.md b/README.md index 15d46f1..7ed226f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ ggplot(df, aes(x='x', y='y')) + geom_point() + facet_wrap('group') ## ggplot2 Function Coverage -### Geoms (44) +### Geoms (46) | Function | Description | |----------|-------------| @@ -50,7 +50,9 @@ ggplot(df, aes(x='x', y='y')) + geom_point() + facet_wrap('group') | `geom_ribbon` | Ribbons with ymin/ymax | | `geom_smooth` | Smoothed lines (LOESS, linear) | | `geom_tile` | Rectangular tiles/heatmaps | +| `geom_rect` | Rectangles (xmin/xmax/ymin/ymax) | | `geom_text` | Text labels | +| `geom_label` | Text labels with background | | `geom_errorbar` | Error bars | | `geom_segment` | Line segments | | `geom_step` | Step plots | @@ -66,13 +68,13 @@ ggplot(df, aes(x='x', y='y')) + geom_point() + facet_wrap('group') | `geom_range` | Range plots (min/max/avg) | | `geom_edgebundle` | Edge bundling for networks | | `geom_searoute` | Maritime shipping routes | +| `geom_sankey` | Sankey flow diagrams | | `geom_point_3d` | 3D scatter plots | | `geom_surface` | 3D surface plots | | `geom_wireframe` | 3D wireframe plots | | `geom_candlestick` | Candlestick charts (financial) | | `geom_ohlc` | OHLC charts (financial) | | `geom_waterfall` | Waterfall charts (financial) | -| `geom_sankey` | Sankey flow diagrams | | `geom_fanchart` | Fan charts for uncertainty | | `geom_stl` | STL decomposition plots | | `geom_acf` | Autocorrelation function plots | @@ -99,7 +101,7 @@ ggplot(df, aes(x='x', y='y')) + geom_point() + facet_wrap('group') | `stat_qq` | Compute Q-Q quantiles | | `stat_qq_line` | Compute Q-Q reference line | -### Scales (17) +### Scales (19) | Function | Description | |----------|-------------| @@ -107,6 +109,8 @@ ggplot(df, aes(x='x', y='y')) + geom_point() + facet_wrap('group') | `scale_y_continuous` | Continuous y-axis | | `scale_x_log10` | Log10 x-axis | | `scale_y_log10` | Log10 y-axis | +| `scale_x_reverse` | Reversed x-axis | +| `scale_y_reverse` | Reversed y-axis | | `scale_x_date` | Date x-axis | | `scale_x_datetime` | DateTime x-axis | | `scale_x_rangeslider` | Interactive range slider for zooming | @@ -121,11 +125,12 @@ ggplot(df, aes(x='x', y='y')) + geom_point() + facet_wrap('group') | `scale_shape_manual` | Manual shape mapping | | `scale_size` | Size scaling | -### Coordinates (4) +### Coordinates (5) | Function | Description | |----------|-------------| -| `coord_cartesian` | Cartesian coordinates | +| `coord_cartesian` | Cartesian coordinates with zoom | +| `coord_fixed` | Fixed aspect ratio coordinates | | `coord_flip` | Flip x and y axes | | `coord_polar` | Polar coordinates | | `coord_sf` | Spatial/geographic coordinates | @@ -151,16 +156,25 @@ ggplot(df, aes(x='x', y='y')) + geom_point() + facet_wrap('group') | `theme_custom` | Custom theme builder | | `theme` | Theme modification | -### Position Adjustments (6) +### Theme Elements (3) + +| Function | Description | +|----------|-------------| +| `element_text` | Text element styling | +| `element_line` | Line element styling | +| `element_rect` | Rectangle element styling | + +### Position Adjustments (7) | Function | Description | |----------|-------------| +| `position_identity` | No adjustment (identity) | | `position_dodge` | Dodge overlapping objects | +| `position_dodge2` | Dodge with variable widths | | `position_jitter` | Add random noise | | `position_stack` | Stack objects | | `position_fill` | Stack and normalize to 100% | | `position_nudge` | Nudge points by fixed amount | -| `position_identity` | No adjustment (identity) | ### Guides & Labels (7) @@ -181,4 +195,14 @@ ggplot(df, aes(x='x', y='y')) + geom_point() + facet_wrap('group') | `ggsave` | Save plots to file | | `ggsize` | Set plot dimensions | -## Total: ~113 ggplot2-equivalent functions +### Other + +| Function | Description | +|----------|-------------| +| `aes` | Aesthetic mappings | +| `after_stat` | Reference computed statistics | +| `layer` | Create custom layers | +| `map_data` | Load map data | +| `data` | Access built-in datasets | + +## Total: ~120 ggplot2-equivalent functions diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..d6cab23 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,205 @@ +# GGPLOTLY Roadmap + +## Current Status: v0.3.5 (Beta) + +--- + +## Changelog + +### [Unreleased] + +#### Added +- `geom_rect` for drawing rectangles (highlight regions, backgrounds) +- `geom_label` for text labels with background boxes +- `scale_x_reverse` and `scale_y_reverse` for reversed axes +- `coord_fixed` for fixed aspect ratio plots +- `stroke` parameter for `geom_point` (marker border width) +- `arrow` and `arrow_size` parameters for `geom_segment` +- `width` parameter for `geom_errorbar` (cap width) +- `parse` parameter for `geom_text` (MathJax/LaTeX support) +- `linewidth` as alias for `size` (ggplot2 3.4+ compatibility) +- Exported position functions: `position_fill`, `position_nudge`, `position_identity`, `position_dodge2` +- Parameter audit complete: standardized `na_rm`, `show_legend`, `colour` alias across all geoms +- `width` parameter for `geom_col` (bar width control) +- `fullrange` parameter for `geom_smooth` (extend to full x-axis) +- `position` parameter for `geom_area` (stacking support) +- Updated docstrings with `linewidth` alias documentation +- 36 new tests for parameter audit features +- Comprehensive MkDocs documentation +- GitHub Actions workflow for automatic docs deployment +- pyproject.toml for modern Python packaging +- 106 new tests with full coverage (parameters + geom_rect/label) + +#### Changed +- Migrated from setup.py to pyproject.toml + +### [0.3.1] - 2024-12-11 + +#### Added +- Automatic Pandas index handling (`x='index'` or automatic detection) +- `geom_range` for 5-year historical range plots +- `geom_searoute` for maritime shipping routes +- `geom_edgebundle` for network visualization +- `geom_candlestick` and `geom_ohlc` for financial charts +- `geom_point_3d`, `geom_surface`, `geom_wireframe` for 3D plots +- `scale_x_rangeslider` and `scale_x_rangeselector` for interactive time series +- `coord_sf` for map projections +- 15 built-in datasets (mpg, diamonds, iris, mtcars, economics, etc.) +- `guide_legend` and `guide_colorbar` for legend customization +- Facet labellers (`label_both`, `label_value`) + +#### Changed +- Improved faceting with consistent colors across panels + +### [0.3.0] - 2024-11-01 + +#### Added +- `geom_contour` and `geom_contour_filled` +- `geom_jitter` and `geom_rug` +- `geom_abline` for diagonal reference lines +- `position_dodge`, `position_stack`, `position_fill`, `position_nudge` +- `stat_summary` and `stat_ecdf` +- `scale_color_gradient` and `scale_fill_viridis_c` +- `theme_bbc` and `theme_nytimes` + +### [0.2.0] - 2024-09-01 + +#### Added +- `geom_map` and `geom_sf` for geographic data +- `coord_polar` for pie charts +- `scale_x_date` and `scale_x_datetime` +- `scale_color_brewer` and `scale_fill_brewer` +- `annotate` function for text and shape annotations + +### [0.1.0] - 2024-08-01 + +#### Added +- Initial release +- Core ggplot grammar: `ggplot`, `aes`, `+` operator +- Basic geoms: point, line, bar, histogram, boxplot, violin, area, ribbon +- Scales: continuous, log10, manual colors +- Themes: minimal, classic, dark, ggplot2 +- Faceting: `facet_wrap`, `facet_grid` +- Labels: `labs`, `ggtitle` +- Utilities: `ggsave`, `ggsize` + +--- + +## Roadmap to v1.0.0 + +### Must-Have (Required for 1.0) + +| Item | Type | Description | Status | +|------|------|-------------|--------| +| `geom_rect` | Geom | Rectangles for highlighting regions | DONE | +| `geom_label` | Geom | Text labels with background box | DONE | +| `scale_x_reverse` | Scale | Reversed x-axis | DONE | +| `scale_y_reverse` | Scale | Reversed y-axis | DONE | +| `coord_fixed` | Coord | Fixed aspect ratio (essential for maps) | DONE | +| Parameter audit | Quality | Review all geom params vs ggplot2 | DONE | + +### Nice-to-Have (Target 1.0, can defer) + +| Item | Type | Description | Status | +|------|------|-------------|--------| +| `geom_polygon` | Geom | Arbitrary polygons | TODO | +| `geom_dotplot` | Geom | Dot plots | TODO | +| `geom_freqpoly` | Geom | Frequency polygons | TODO | +| `geom_spoke` | Geom | Line segments by angle | TODO | +| `geom_curve` | Geom | Curved line segments | TODO | +| `scale_alpha` | Scale | Alpha/transparency scaling | TODO | +| `scale_linetype` | Scale | Linetype scaling | TODO | +| `scale_x_sqrt` | Scale | Square root x-axis | TODO | +| `scale_y_sqrt` | Scale | Square root y-axis | TODO | +| `scale_color_viridis_c` | Scale | Viridis for color aesthetic | TODO | +| `scale_color_distiller` | Scale | ColorBrewer continuous for color | TODO | +| `coord_trans` | Coord | Transformed coordinates | TODO | +| `stat_boxplot` | Stat | Boxplot statistics | TODO | +| `stat_unique` | Stat | Remove duplicates | TODO | + +--- + +## Future Roadmap (Post 1.0) + +### v1.1 - Enhanced Interactivity +- [ ] Animation slider (Plotly animation frames) +- [ ] Dropdown selectors for data filtering +- [ ] Linked brushing between plots +- [ ] Custom hover templates via parameter + +### v1.2 - Advanced Geoms +- [ ] `geom_density_2d` - 2D density estimation +- [ ] `geom_hex` - Hexagonal binning +- [ ] `geom_quantile` - Quantile regression lines +- [ ] `geom_crossbar` - Crossbar error bars + +### v1.3 - Statistical Extensions +- [ ] `stat_bin_2d` - 2D binning +- [ ] `stat_ellipse` - Confidence ellipses +- [ ] `stat_function` enhancements +- [ ] Better integration with statsmodels + +### v1.4 - Theming & Polish +- [ ] `element_*` functions for fine-grained theming +- [ ] More theme presets (economist, fivethirtyeight, etc.) +- [ ] Better default color palettes +- [ ] Improved legend positioning + +--- + +## Completed Features + +### Geoms (46+) +- Basic: `geom_point`, `geom_line`, `geom_path`, `geom_bar`, `geom_col`, `geom_area`, `geom_ribbon` +- Distribution: `geom_histogram`, `geom_density`, `geom_boxplot`, `geom_violin`, `geom_qq` +- Statistical: `geom_smooth`, `geom_errorbar`, `geom_pointrange`, `geom_linerange` +- Annotation: `geom_text`, `geom_label`, `geom_rect`, `geom_hline`, `geom_vline`, `geom_abline`, `geom_segment` +- Specialized: `geom_tile`, `geom_raster`, `geom_contour`, `geom_contour_filled` +- Financial: `geom_candlestick`, `geom_ohlc`, `geom_waterfall` +- 3D: `geom_point_3d`, `geom_surface`, `geom_wireframe` +- Geographic: `geom_map`, `geom_sf`, `geom_searoute` +- Network: `geom_edgebundle`, `geom_sankey` +- Other: `geom_step`, `geom_jitter`, `geom_rug`, `geom_range` + +### Stats (13) +`stat_identity`, `stat_count`, `stat_bin`, `stat_density`, `stat_smooth`, `stat_summary`, `stat_ecdf`, `stat_function`, `stat_qq`, `stat_stl`, `stat_spoke`, `stat_bin_2d`, `stat_contour` + +### Scales (19+) +- Continuous: `scale_x_continuous`, `scale_y_continuous`, `scale_x_log10`, `scale_y_log10` +- Reversed: `scale_x_reverse`, `scale_y_reverse` +- Date/Time: `scale_x_date`, `scale_x_datetime` +- Color: `scale_color_manual`, `scale_color_gradient`, `scale_color_brewer`, `scale_fill_*` variants +- Viridis: `scale_fill_viridis_c`, `scale_fill_viridis_d` +- Interactive: `scale_x_rangeslider`, `scale_x_rangeselector` + +### Coords (5) +`coord_cartesian`, `coord_fixed`, `coord_flip`, `coord_polar`, `coord_sf` + +### Positions (7) +`position_identity`, `position_dodge`, `position_dodge2`, `position_stack`, `position_fill`, `position_jitter`, `position_nudge` + +### Themes (9) +`theme_default`, `theme_minimal`, `theme_classic`, `theme_dark`, `theme_ggplot2`, `theme_bw`, `theme_void`, `theme_bbc`, `theme_nytimes` + +### Other +- Faceting: `facet_wrap`, `facet_grid` with labellers +- Guides: `guides`, `guide_legend`, `guide_colorbar` +- Labels: `labs`, `ggtitle`, `xlab`, `ylab` +- Utilities: `ggsave`, `ggsize`, `annotate` +- 16 built-in datasets + +--- + +## Contributing + +When implementing new features: +1. Follow ggplot2 conventions (see [CLAUDE.md](CLAUDE.md)) +2. Add tests covering all 4 categories (basic, edge cases, integration, visual regression) +3. Update this roadmap when features are completed +4. Add examples to `examples/` directory + +## Links + +- [ggplot2 Reference](https://ggplot2.tidyverse.org/reference/) +- [Plotly Python](https://plotly.com/python/) +- [GitHub Repository](https://github.com/bbcho/ggplotly) diff --git a/TODO.md b/TODO.md deleted file mode 100644 index c4abb0d..0000000 --- a/TODO.md +++ /dev/null @@ -1,65 +0,0 @@ -# TODO - -- [ ] go thru geoms one by one to make sure I've replicated all of the parameters from ggplot -- [ ] Add plotly charts that don't exist in ggplot2 like 3D scatter plots - - [ ] sliders and range controls - - [ ] 3D plots - - [ ] waterfall charts - -## Base - -- [X] deal with datetime index and Series - - -## Plotly Functionality I want to port over - -- [X] range slider -- [X] range selector -- [ ] slider -- [ ] drop down - -## Missing ggplot2 Functions - -### Geoms - -- [ ] `geom_polygon` - Polygons -- [ ] `geom_rect` - Rectangles -- [ ] `geom_label` - Text labels with background -- [ ] `geom_dotplot` - Dot plots -- [ ] `geom_freqpoly` - Frequency polygons -- [ ] `geom_qq` - Q-Q plots -- [ ] `geom_spoke` - Line segments parameterized by angle -- [ ] `geom_curve` - Curved line segments - -### Stats - -- [ ] `stat_boxplot` - Boxplot statistics -- [ ] `stat_qq` - Q-Q plot statistics -- [ ] `stat_unique` - Remove duplicates - -### Scales - -- [ ] `scale_alpha` - Alpha/transparency scaling -- [ ] `scale_linetype` - Linetype scaling -- [ ] `scale_x_reverse` / `scale_y_reverse` - Reversed axes -- [ ] `scale_x_sqrt` / `scale_y_sqrt` - Square root transformation -- [ ] `scale_color_viridis_c` - Viridis for color (currently only fill) -- [ ] `scale_color_distiller` - ColorBrewer continuous - -### Coordinates - -- [ ] `coord_fixed` - Fixed aspect ratio -- [ ] `coord_trans` - Transformed coordinates -- [ ] `coord_sf` - Spatial coordinates - -### Positions - -- [ ] `position_fill` - Stack and normalize to 100% -- [ ] `position_nudge` - Nudge points by fixed amount -- [ ] `position_identity` - No adjustment - -### Guides - -- [ ] `guides` - Customize legends/colorbars -- [ ] `guide_legend` - Legend customization -- [ ] `guide_colorbar` - Colorbar customization diff --git a/docs/api/coords.md b/docs/api/coords.md index 3895922..bd1f3e2 100644 --- a/docs/api/coords.md +++ b/docs/api/coords.md @@ -8,6 +8,10 @@ Coordinate systems control how data coordinates are mapped to the plane of the g options: show_root_heading: true +::: ggplotly.coords.coord_fixed.coord_fixed + options: + show_root_heading: true + ::: ggplotly.coords.coord_flip.coord_flip options: show_root_heading: true diff --git a/docs/api/geoms.md b/docs/api/geoms.md index c9812bd..4d04f34 100644 --- a/docs/api/geoms.md +++ b/docs/api/geoms.md @@ -68,6 +68,10 @@ Geometric objects (geoms) are the visual elements used to represent data in a pl options: show_root_heading: true +::: ggplotly.geoms.geom_rect.geom_rect + options: + show_root_heading: true + ## Statistical Geoms ::: ggplotly.geoms.geom_smooth.geom_smooth @@ -88,6 +92,10 @@ Geometric objects (geoms) are the visual elements used to represent data in a pl options: show_root_heading: true +::: ggplotly.geoms.geom_label.geom_label + options: + show_root_heading: true + ## Reference Lines ::: ggplotly.geoms.geom_hline.geom_hline diff --git a/docs/api/scales.md b/docs/api/scales.md index 30ec138..cb5a476 100644 --- a/docs/api/scales.md +++ b/docs/api/scales.md @@ -22,6 +22,16 @@ Scales control how data values are mapped to visual properties like position, co options: show_root_heading: true +## Reversed Scales + +::: ggplotly.scales.scale_x_reverse.scale_x_reverse + options: + show_root_heading: true + +::: ggplotly.scales.scale_y_reverse.scale_y_reverse + options: + show_root_heading: true + ## Date and Time Scales ::: ggplotly.scales.scale_x_date.scale_x_date diff --git a/docs/guide/coordinates.ipynb b/docs/guide/coordinates.ipynb index b4ee9df..3147dc2 100644 --- a/docs/guide/coordinates.ipynb +++ b/docs/guide/coordinates.ipynb @@ -66,13 +66,26 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "**xlim/ylim vs coord_cartesian:** `xlim()` and `ylim()` filter the data before plotting. `coord_cartesian()` zooms the view without removing data points. This matters for statistical calculations like `geom_smooth`.\n", - "\n", - "## coord_flip\n", - "\n", - "Flip x and y axes." - ] + "source": "**xlim/ylim vs coord_cartesian:** `xlim()` and `ylim()` filter the data before plotting. `coord_cartesian()` zooms the view without removing data points. This matters for statistical calculations like `geom_smooth`.\n\n## coord_fixed\n\nFixed aspect ratio coordinates. Essential for maps and any plot where x and y must have equal scaling." + }, + { + "cell_type": "code", + "source": "# Square data should appear as a square with ratio=1\nsquare_df = pd.DataFrame({'x': [0, 1, 1, 0, 0], 'y': [0, 0, 1, 1, 0]})\nggplot(square_df, aes(x='x', y='y')) + geom_path(size=2) + geom_point(size=10) + coord_fixed()", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": "# With ratio=2, the square appears as a tall rectangle (y is stretched)\nggplot(square_df, aes(x='x', y='y')) + geom_path(size=2) + geom_point(size=10) + coord_fixed(ratio=2)", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": "## coord_flip\n\nFlip x and y axes. Useful for horizontal bar charts and boxplots.", + "metadata": {} }, { "cell_type": "code", @@ -104,11 +117,11 @@ ] }, { - "cell_type": "code", + "cell_type": "markdown", "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Pie chart\npie_df = pd.DataFrame({\n 'category': ['A', 'B', 'C', 'D'],\n 'value': [25, 30, 20, 25]\n})\npie_df['x'] = 1 # Constant x for stacked bar -> pie conversion\n\nggplot(pie_df, aes(x='x', y='value', fill='category')) + \\\n geom_bar(stat='identity', width=1) + \\\n coord_polar(theta='y')" + "source": "### coord_polar Parameters\n\n```python\ncoord_polar(\n theta='x', # Variable mapped to angle ('x' or 'y')\n start=0, # Starting angle in radians\n direction=1 # 1 = clockwise, -1 = counter-clockwise\n)\n```\n\n## coord_sf\n\nFor geographic/spatial data with proper map projections. Requires geopandas.\n\n```python\nimport geopandas as gpd\n\n# Load geographic data\nworld = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))\n\n# Plot with projection\nggplot(world) + geom_sf() + coord_sf(crs='EPSG:4326')\n```\n\n### Available Projections\n\n- `'mercator'` - Web Mercator (Google Maps style)\n- `'albers usa'` - Albers USA (good for US maps)\n- `'orthographic'` - Globe view\n- `'natural earth'` - Natural Earth projection\n- `'robinson'` - Robinson projection\n\n## Coordinate Reference\n\n| Function | Description |\n|----------|-------------|\n| `coord_cartesian` | Default Cartesian, zoom without clipping |\n| `coord_fixed` | Fixed aspect ratio (ratio=1 for equal scaling) |\n| `coord_flip` | Flip x and y axes |\n| `coord_polar` | Polar coordinates |\n| `coord_sf` | Geographic projections |" }, { "cell_type": "markdown", diff --git a/docs/guide/geoms.ipynb b/docs/guide/geoms.ipynb index 0576b19..f4b2931 100644 --- a/docs/guide/geoms.ipynb +++ b/docs/guide/geoms.ipynb @@ -3,7 +3,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Geoms\n\nGeoms (geometric objects) are the visual elements that represent your data. ggplotly provides 44 geoms for different visualization types." + "source": "# Geoms\n\nGeoms (geometric objects) are the visual elements that represent your data. ggplotly provides 46 geoms for different visualization types." }, { "cell_type": "code", @@ -316,6 +316,18 @@ "ggplot(ribbon_df, aes(x='date', ymin='lower', ymax='upper')) + geom_ribbon(alpha=0.3)" ] }, + { + "cell_type": "markdown", + "source": "### geom_rect\n\nRectangles defined by xmin, xmax, ymin, ymax.", + "metadata": {} + }, + { + "cell_type": "code", + "source": "# Rectangle data\nrect_df = pd.DataFrame({\n 'xmin': [0, 2, 4],\n 'xmax': [1, 3, 5],\n 'ymin': [0, 1, 0.5],\n 'ymax': [2, 3, 2.5],\n 'category': ['A', 'B', 'C']\n})\n\nggplot(rect_df, aes(xmin='xmin', xmax='xmax', ymin='ymin', ymax='ymax', fill='category')) + geom_rect(alpha=0.7)", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, { "cell_type": "markdown", "metadata": {}, @@ -366,6 +378,18 @@ "ggplot(bar_df, aes(x='category', y='count', label='count')) + geom_col() + geom_text()" ] }, + { + "cell_type": "markdown", + "source": "### geom_label\n\nText labels with a background box.", + "metadata": {} + }, + { + "cell_type": "code", + "source": "ggplot(bar_df, aes(x='category', y='count', label='count')) + geom_col() + geom_label()", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, { "cell_type": "markdown", "metadata": {}, @@ -559,7 +583,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## Complete Geom List\n\n| Geom | Description |\n|------|-------------|\n| `geom_point` | Scatter plots |\n| `geom_line` | Line plots (sorted by x) |\n| `geom_lines` | Multi-series line plots |\n| `geom_path` | Path plots (data order) |\n| `geom_bar` | Bar charts |\n| `geom_col` | Column charts |\n| `geom_histogram` | Histograms |\n| `geom_boxplot` | Box plots |\n| `geom_violin` | Violin plots |\n| `geom_density` | Density plots |\n| `geom_area` | Area plots |\n| `geom_ribbon` | Ribbon plots |\n| `geom_smooth` | Smoothed lines |\n| `geom_tile` | Heatmaps |\n| `geom_text` | Text labels |\n| `geom_errorbar` | Error bars |\n| `geom_segment` | Line segments |\n| `geom_step` | Step plots |\n| `geom_rug` | Rug plots |\n| `geom_jitter` | Jittered points |\n| `geom_vline` | Vertical lines |\n| `geom_hline` | Horizontal lines |\n| `geom_abline` | Diagonal lines |\n| `geom_contour` | Contour lines |\n| `geom_contour_filled` | Filled contours |\n| `geom_map` | Choropleth maps |\n| `geom_sf` | Simple features |\n| `geom_range` | Range plots |\n| `geom_edgebundle` | Edge bundling |\n| `geom_searoute` | Sea routes |\n| `geom_fanchart` | Fan charts for uncertainty |\n| `geom_point_3d` | 3D points |\n| `geom_surface` | 3D surfaces |\n| `geom_wireframe` | 3D wireframes |\n| `geom_candlestick` | Candlestick charts |\n| `geom_ohlc` | OHLC charts |\n| `geom_waterfall` | Waterfall charts |\n| `geom_sankey` | Sankey flow diagrams |\n| `geom_stl` | STL decomposition plots |\n| `geom_acf` | Autocorrelation plots |\n| `geom_pacf` | Partial autocorrelation plots |\n| `geom_norm` | Normal distribution overlay |\n| `geom_qq` | Q-Q plots |\n| `geom_qq_line` | Q-Q reference line |" + "source": "## Complete Geom List\n\n| Geom | Description |\n|------|-------------|\n| `geom_point` | Scatter plots |\n| `geom_line` | Line plots (sorted by x) |\n| `geom_lines` | Multi-series line plots |\n| `geom_path` | Path plots (data order) |\n| `geom_bar` | Bar charts |\n| `geom_col` | Column charts |\n| `geom_histogram` | Histograms |\n| `geom_boxplot` | Box plots |\n| `geom_violin` | Violin plots |\n| `geom_density` | Density plots |\n| `geom_area` | Area plots |\n| `geom_ribbon` | Ribbon plots |\n| `geom_rect` | Rectangles |\n| `geom_smooth` | Smoothed lines |\n| `geom_tile` | Heatmaps |\n| `geom_text` | Text labels |\n| `geom_label` | Text labels with background |\n| `geom_errorbar` | Error bars |\n| `geom_segment` | Line segments |\n| `geom_step` | Step plots |\n| `geom_rug` | Rug plots |\n| `geom_jitter` | Jittered points |\n| `geom_vline` | Vertical lines |\n| `geom_hline` | Horizontal lines |\n| `geom_abline` | Diagonal lines |\n| `geom_contour` | Contour lines |\n| `geom_contour_filled` | Filled contours |\n| `geom_map` | Choropleth maps |\n| `geom_sf` | Simple features |\n| `geom_range` | Range plots |\n| `geom_edgebundle` | Edge bundling |\n| `geom_searoute` | Sea routes |\n| `geom_fanchart` | Fan charts for uncertainty |\n| `geom_point_3d` | 3D points |\n| `geom_surface` | 3D surfaces |\n| `geom_wireframe` | 3D wireframes |\n| `geom_candlestick` | Candlestick charts |\n| `geom_ohlc` | OHLC charts |\n| `geom_waterfall` | Waterfall charts |\n| `geom_sankey` | Sankey flow diagrams |\n| `geom_stl` | STL decomposition plots |\n| `geom_acf` | Autocorrelation plots |\n| `geom_pacf` | Partial autocorrelation plots |\n| `geom_norm` | Normal distribution overlay |\n| `geom_qq` | Q-Q plots |\n| `geom_qq_line` | Q-Q reference line |" } ], "metadata": { diff --git a/examples/edgebundleexample/EARLY_FILTERING_RESULTS.md b/edgebundleexample/EARLY_FILTERING_RESULTS.md similarity index 100% rename from examples/edgebundleexample/EARLY_FILTERING_RESULTS.md rename to edgebundleexample/EARLY_FILTERING_RESULTS.md diff --git a/examples/edgebundleexample/FINAL_VECTORIZATION_RESULTS.md b/edgebundleexample/FINAL_VECTORIZATION_RESULTS.md similarity index 100% rename from examples/edgebundleexample/FINAL_VECTORIZATION_RESULTS.md rename to edgebundleexample/FINAL_VECTORIZATION_RESULTS.md diff --git a/examples/edgebundleexample/PYTHON_README.md b/edgebundleexample/PYTHON_README.md similarity index 100% rename from examples/edgebundleexample/PYTHON_README.md rename to edgebundleexample/PYTHON_README.md diff --git a/examples/edgebundleexample/VECTORIZATION_RESULTS.md b/edgebundleexample/VECTORIZATION_RESULTS.md similarity index 100% rename from examples/edgebundleexample/VECTORIZATION_RESULTS.md rename to edgebundleexample/VECTORIZATION_RESULTS.md diff --git a/examples/edgebundleexample/VISIBILITY_ANALYSIS.md b/edgebundleexample/VISIBILITY_ANALYSIS.md similarity index 100% rename from examples/edgebundleexample/VISIBILITY_ANALYSIS.md rename to edgebundleexample/VISIBILITY_ANALYSIS.md diff --git a/examples/edgebundleexample/edge_bundle.py b/edgebundleexample/edge_bundle.py similarity index 100% rename from examples/edgebundleexample/edge_bundle.py rename to edgebundleexample/edge_bundle.py diff --git a/examples/edgebundleexample/edge_bundling_demo.ipynb b/edgebundleexample/edge_bundling_demo.ipynb similarity index 100% rename from examples/edgebundleexample/edge_bundling_demo.ipynb rename to edgebundleexample/edge_bundling_demo.ipynb diff --git a/examples/edgebundleexample/example_usage.py b/edgebundleexample/example_usage.py similarity index 100% rename from examples/edgebundleexample/example_usage.py rename to edgebundleexample/example_usage.py diff --git a/examples/edgebundleexample/test_additional_vectorizations.py b/edgebundleexample/test_additional_vectorizations.py similarity index 100% rename from examples/edgebundleexample/test_additional_vectorizations.py rename to edgebundleexample/test_additional_vectorizations.py diff --git a/examples/edgebundleexample/test_early_filtering.py b/edgebundleexample/test_early_filtering.py similarity index 100% rename from examples/edgebundleexample/test_early_filtering.py rename to edgebundleexample/test_early_filtering.py diff --git a/examples/edgebundleexample/test_edge_bundle.py b/edgebundleexample/test_edge_bundle.py similarity index 100% rename from examples/edgebundleexample/test_edge_bundle.py rename to edgebundleexample/test_edge_bundle.py diff --git a/examples/edgebundleexample/test_validation.py b/edgebundleexample/test_validation.py similarity index 100% rename from examples/edgebundleexample/test_validation.py rename to edgebundleexample/test_validation.py diff --git a/examples/edgebundleexample/test_vectorization.py b/edgebundleexample/test_vectorization.py similarity index 100% rename from examples/edgebundleexample/test_vectorization.py rename to edgebundleexample/test_vectorization.py diff --git a/examples/edgebundleexample/us_flights_edges.csv b/edgebundleexample/us_flights_edges.csv similarity index 100% rename from examples/edgebundleexample/us_flights_edges.csv rename to edgebundleexample/us_flights_edges.csv diff --git a/examples/edgebundleexample/us_flights_example.py b/edgebundleexample/us_flights_example.py similarity index 100% rename from examples/edgebundleexample/us_flights_example.py rename to edgebundleexample/us_flights_example.py diff --git a/examples/edgebundleexample/us_flights_nodes.csv b/edgebundleexample/us_flights_nodes.csv similarity index 100% rename from examples/edgebundleexample/us_flights_nodes.csv rename to edgebundleexample/us_flights_nodes.csv diff --git a/examples/edgebundleexample/vectorization_analysis.md b/edgebundleexample/vectorization_analysis.md similarity index 100% rename from examples/edgebundleexample/vectorization_analysis.md rename to edgebundleexample/vectorization_analysis.md diff --git a/examples/EdgeBundling.ipynb b/examples/EdgeBundling.ipynb index 176bbea..3b09af9 100644 --- a/examples/EdgeBundling.ipynb +++ b/examples/EdgeBundling.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "33b48acf", "metadata": {}, "outputs": [], @@ -25,1006 +25,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "c0353609", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Bundling 3 edges...\n", - "Initial edge division (P=1)...\n", - "Computing angle compatibility...\n", - "Computing scale compatibility...\n", - "Computing position compatibility...\n", - "Filtering candidate pairs...\n", - "Computing visibility for 0 candidate pairs (out of 3 total)\n", - "Filtered out 3 pairs (100.0%)\n", - "Computing final compatibility scores...\n", - "Compatibility matrix: 0 compatible pairs\n", - "Cycle 1/6: I=50, P=1, S=0.0400\n", - "Updating subdivisions (new P=2)...\n", - "Cycle 2/6: I=33, P=2, S=0.0200\n", - "Updating subdivisions (new P=4)...\n", - "Cycle 3/6: I=22, P=4, S=0.0100\n", - "Updating subdivisions (new P=8)...\n", - "Cycle 4/6: I=14, P=8, S=0.0050\n", - "Updating subdivisions (new P=16)...\n", - "Cycle 5/6: I=9, P=16, S=0.0025\n", - "Updating subdivisions (new P=32)...\n", - "Cycle 6/6: I=6, P=32, S=0.0013\n", - "Assembling output...\n", - "Done!\n", - "Bundling 3 edges...\n", - "Initial edge division (P=1)...\n", - "Computing angle compatibility...\n", - "Computing scale compatibility...\n", - "Computing position compatibility...\n", - "Filtering candidate pairs...\n", - "Computing visibility for 0 candidate pairs (out of 3 total)\n", - "Filtered out 3 pairs (100.0%)\n", - "Computing final compatibility scores...\n", - "Compatibility matrix: 0 compatible pairs\n", - "Cycle 1/6: I=50, P=1, S=0.0400\n", - "Updating subdivisions (new P=2)...\n", - "Cycle 2/6: I=33, P=2, S=0.0200\n", - "Updating subdivisions (new P=4)...\n", - "Cycle 3/6: I=22, P=4, S=0.0100\n", - "Updating subdivisions (new P=8)...\n", - "Cycle 4/6: I=14, P=8, S=0.0050\n", - "Updating subdivisions (new P=16)...\n", - "Cycle 5/6: I=9, P=16, S=0.0025\n", - "Updating subdivisions (new P=32)...\n", - "Cycle 6/6: I=6, P=32, S=0.0013\n", - "Assembling output...\n", - "Done!\n" - ] - }, - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.7)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "mpmZmZmZyT/D2jesfcPKP+0b1r5h7cs/GF100UUXzT9CnhLkKUHOP2vfsPYNa88/SpCnBHlK0D/fsPYNa9/QP3XRRRdddNE/CvKUIE8J0j+eEuQpQZ7SPzMzMzMzM9M/yFOCPCXI0z9ddNFFF13UP/KUIE8J8tQ/h7VvWPuG1T8c1r5h7RvWP7H2DWvfsNY/RhdddNFF1z/bN6x9w9rXP3BY+4a1b9g/BXlKkKcE2T+amZmZmZnZPy+66KKLLto/xNo3rH3D2j9Z+4a1b1jbP+4b1r5h7ds/gzwlyFOC3D8YXXTRRRfdP619w9o3rN0/Qp4S5ClB3j/XvmHtG9beP2zfsPYNa98/AAAAAAAA4D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "MzMzMzMz4z/ooosuuujiP54S5ClBnuI/VII8JchT4j8J8pQgTwniP75h7RvWvuE/dNFFF1104T8qQZ4S5CnhP9+w9g1r3+A/lCBPCfKU4D9KkKcEeUrgP////////98/at+w9g1r3z/VvmHtG9beP0CeEuQpQd4/q33D2jes3T8WXXTRRRfdP4E8JchTgtw/7BvWvmHt2z9X+4a1b1jbP8PaN6x9w9o/Lrrooosu2j+ZmZmZmZnZPwR5SpCnBNk/b1j7hrVv2D/aN6x9w9rXP0UXXXTRRdc/sPYNa9+w1j8b1r5h7RvWP4a1b1j7htU/8ZQgTwny1D9cdNFFF13UP8dTgjwlyNM/MzMzMzMz0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.7)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA4D9XKG6F4lbgP65Q3ArFreA/BXlKkKcE4T9cobgVilvhP7PJJptssuE/CfKUIE8J4j9gGgOmMWDiP7dCcSsUt+I/DmvfsPYN4z9lk0022WTjP7y7u7u7u+M/E+QpQZ4S5D9qDJjGgGnkP8A0BkxjwOQ/F1100UUX5T9uheJWKG7lP8WtUNwKxeU/HNa+Ye0b5j9z/iznz3LmP8omm2yyyeY/IU8J8pQg5z93d3d3d3fnP86f5fxZzuc/JchTgjwl6D988MEHH3zoP9MYMI0B0+g/KkGeEuQp6T+BaQyYxoDpP9iReh2p1+k/Lrrooosu6j+F4lYoboXqP9wKxa1Q3Oo/MzMzMzMz6z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "MzMzMzMz0z8C0xgwjQHTP9By/iznz9I/nhLkKUGe0j9tsskmm2zSPzxSryP1OtI/CvKUIE8J0j/YkXodqdfRP6YxYBoDptE/dNFFF1100T9DcSsUt0LRPxEREREREdE/37D2DWvf0D+tUNwKxa3QP3zwwQcffNA/SpCnBHlK0D8ZMI0B0xjQP8+f5fxZzs8/at+w9g1rzz8GH3zwwQfPP6NeR+p1pM4/QJ4S5ClBzj/e3d3d3d3NP3sdqdeRes0/GF100UUXzT+1nD/L+bPMP1LcCsWtUMw/7xvWvmHtyz+LW6G4FYrLPyibbLLJJss/xNo3rH3Dyj9gGgOmMWDKP/1Zzp/l/Mk/mpmZmZmZyT8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.7)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "zczMzMzM7D8ffPDBBx/sP3ErFLdCces/w9o3rH3D6j8WiluhuBXqP2g5f5bzZ+k/uuiiiy666D8MmMaAaQzoP19H6nWkXuc/sfYNa9+w5j8DpjFgGgPmP1VVVVVVVeU/qAR5SpCn5D/6s5w/y/njP0xjwDQGTOM/nhLkKUGe4j/xwQcffPDhP0NxKxS3QuE/lSBPCfKU4D/Pn+X8Wc7fP3T+LOfPct4/GF100UUX3T+8u7u7u7vbP2AaA6YxYNo/BXlKkKcE2T+q15F6HanXP0422WSTTdY/8pQgTwny1D+X82c5f5bTPztSryP1OtI/4LD2DWvf0D8JH3zwwQfPP1LcCsWtUMw/mpmZmZmZyT8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA6D/bN6x9w9rnP7ZvWPuGtec/kacEeUqQ5z9s37D2DWvnP0cXXXTRRec/IU8J8pQg5z/8hrVvWPvmP9e+Ye0b1uY/sfYNa9+w5j+MLrrooovmP2dmZmZmZuY/Qp4S5ClB5j8d1r5h7RvmP/cNa9+w9uU/0UUXXXTR5T+sfcPaN6zlP4e1b1j7huU/Yu0b1r5h5T88JchTgjzlPxdddNFFF+U/8pQgTwny5D/NzMzMzMzkP6gEeUqQp+Q/gjwlyFOC5D9cdNFFF13kPzisfcPaN+Q/E+QpQZ4S5D/tG9a+Ye3jP8hTgjwlyOM/o4suuuii4z9+w9o3rH3jP1n7hrVvWOM/MzMzMzMz4z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "mpmZmZmZyT/D2jesfcPKP+0b1r5h7cs/GF100UUXzT9CnhLkKUHOP2vfsPYNa88/SpCnBHlK0D/fsPYNa9/QP3XRRRdddNE/CvKUIE8J0j+eEuQpQZ7SPzMzMzMzM9M/yFOCPCXI0z9ddNFFF13UP/KUIE8J8tQ/h7VvWPuG1T8c1r5h7RvWP7H2DWvfsNY/RhdddNFF1z/bN6x9w9rXP3BY+4a1b9g/BXlKkKcE2T+amZmZmZnZPy+66KKLLto/xNo3rH3D2j9Z+4a1b1jbP+4b1r5h7ds/gzwlyFOC3D8YXXTRRRfdP619w9o3rN0/Qp4S5ClB3j/XvmHtG9beP2zfsPYNa98/AAAAAAAA4D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "MzMzMzMz4z/ooosuuujiP54S5ClBnuI/VII8JchT4j8J8pQgTwniP75h7RvWvuE/dNFFF1104T8qQZ4S5CnhP9+w9g1r3+A/lCBPCfKU4D9KkKcEeUrgP////////98/at+w9g1r3z/VvmHtG9beP0CeEuQpQd4/q33D2jes3T8WXXTRRRfdP4E8JchTgtw/7BvWvmHt2z9X+4a1b1jbP8PaN6x9w9o/Lrrooosu2j+ZmZmZmZnZPwR5SpCnBNk/b1j7hrVv2D/aN6x9w9rXP0UXXXTRRdc/sPYNa9+w1j8b1r5h7RvWP4a1b1j7htU/8ZQgTwny1D9cdNFFF13UP8dTgjwlyNM/MzMzMzMz0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA4D9XKG6F4lbgP65Q3ArFreA/BXlKkKcE4T9cobgVilvhP7PJJptssuE/CfKUIE8J4j9gGgOmMWDiP7dCcSsUt+I/DmvfsPYN4z9lk0022WTjP7y7u7u7u+M/E+QpQZ4S5D9qDJjGgGnkP8A0BkxjwOQ/F1100UUX5T9uheJWKG7lP8WtUNwKxeU/HNa+Ye0b5j9z/iznz3LmP8omm2yyyeY/IU8J8pQg5z93d3d3d3fnP86f5fxZzuc/JchTgjwl6D988MEHH3zoP9MYMI0B0+g/KkGeEuQp6T+BaQyYxoDpP9iReh2p1+k/Lrrooosu6j+F4lYoboXqP9wKxa1Q3Oo/MzMzMzMz6z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "MzMzMzMz0z8C0xgwjQHTP9By/iznz9I/nhLkKUGe0j9tsskmm2zSPzxSryP1OtI/CvKUIE8J0j/YkXodqdfRP6YxYBoDptE/dNFFF1100T9DcSsUt0LRPxEREREREdE/37D2DWvf0D+tUNwKxa3QP3zwwQcffNA/SpCnBHlK0D8ZMI0B0xjQP8+f5fxZzs8/at+w9g1rzz8GH3zwwQfPP6NeR+p1pM4/QJ4S5ClBzj/e3d3d3d3NP3sdqdeRes0/GF100UUXzT+1nD/L+bPMP1LcCsWtUMw/7xvWvmHtyz+LW6G4FYrLPyibbLLJJss/xNo3rH3Dyj9gGgOmMWDKP/1Zzp/l/Mk/mpmZmZmZyT8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "zczMzMzM7D8ffPDBBx/sP3ErFLdCces/w9o3rH3D6j8WiluhuBXqP2g5f5bzZ+k/uuiiiy666D8MmMaAaQzoP19H6nWkXuc/sfYNa9+w5j8DpjFgGgPmP1VVVVVVVeU/qAR5SpCn5D/6s5w/y/njP0xjwDQGTOM/nhLkKUGe4j/xwQcffPDhP0NxKxS3QuE/lSBPCfKU4D/Pn+X8Wc7fP3T+LOfPct4/GF100UUX3T+8u7u7u7vbP2AaA6YxYNo/BXlKkKcE2T+q15F6HanXP0422WSTTdY/8pQgTwny1D+X82c5f5bTPztSryP1OtI/4LD2DWvf0D8JH3zwwQfPP1LcCsWtUMw/mpmZmZmZyT8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA6D/bN6x9w9rnP7ZvWPuGtec/kacEeUqQ5z9s37D2DWvnP0cXXXTRRec/IU8J8pQg5z/8hrVvWPvmP9e+Ye0b1uY/sfYNa9+w5j+MLrrooovmP2dmZmZmZuY/Qp4S5ClB5j8d1r5h7RvmP/cNa9+w9uU/0UUXXXTR5T+sfcPaN6zlP4e1b1j7huU/Yu0b1r5h5T88JchTgjzlPxdddNFFF+U/8pQgTwny5D/NzMzMzMzkP6gEeUqQp+Q/gjwlyFOC5D9cdNFFF13kPzisfcPaN+Q/E+QpQZ4S5D/tG9a+Ye3jP8hTgjwlyOM/o4suuuii4z9+w9o3rH3jP1n7hrVvWOM/MzMzMzMz4z8=", - "dtype": "f8" - }, - "yaxis": "y" - } - ], - "layout": { - "legend": { - "x": 1.02, - "xanchor": "left", - "y": 1, - "yanchor": "top" - }, - "showlegend": true, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermap": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermap" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "xaxis": { - "anchor": "y", - "domain": [ - 0, - 1 - ] - }, - "yaxis": { - "anchor": "x", - "domain": [ - 0, - 1 - ] - } - } - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [], - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from ggplotly import ggplot, aes, geom_edgebundle\n", "\n", @@ -1046,4888 +50,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "595c5959", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Created 100 edges between 20 nodes\n", - "Applying edge bundling...\n", - "Bundling 100 edges...\n", - "Initial edge division (P=1)...\n", - "Computing angle compatibility...\n", - "Computing scale compatibility...\n", - "Computing position compatibility...\n", - "Filtering candidate pairs...\n", - "Computing visibility for 4,065 candidate pairs (out of 4,950 total)\n", - "Filtered out 885 pairs (17.9%)\n", - "Computing final compatibility scores...\n", - "Compatibility matrix: 1966 compatible pairs\n", - "Cycle 1/6: I=50, P=1, S=0.0400\n", - "Updating subdivisions (new P=2)...\n", - "Cycle 2/6: I=33, P=2, S=0.0200\n", - "Updating subdivisions (new P=4)...\n", - "Cycle 3/6: I=22, P=4, S=0.0100\n", - "Updating subdivisions (new P=8)...\n", - "Cycle 4/6: I=14, P=8, S=0.0050\n", - "Updating subdivisions (new P=16)...\n", - "Cycle 5/6: I=9, P=16, S=0.0025\n", - "Updating subdivisions (new P=32)...\n", - "Cycle 6/6: I=6, P=32, S=0.0013\n", - "Assembling output...\n", - "Done!\n", - "Bundling 100 edges...\n", - "Initial edge division (P=1)...\n", - "Computing angle compatibility...\n", - "Computing scale compatibility...\n", - "Computing position compatibility...\n", - "Filtering candidate pairs...\n", - "Computing visibility for 4,065 candidate pairs (out of 4,950 total)\n", - "Filtered out 885 pairs (17.9%)\n", - "Computing final compatibility scores...\n", - "Compatibility matrix: 1966 compatible pairs\n", - "Cycle 1/6: I=50, P=1, S=0.0400\n", - "Updating subdivisions (new P=2)...\n", - "Cycle 2/6: I=33, P=2, S=0.0200\n", - "Updating subdivisions (new P=4)...\n", - "Cycle 3/6: I=22, P=4, S=0.0100\n", - "Updating subdivisions (new P=8)...\n", - "Cycle 4/6: I=14, P=8, S=0.0050\n", - "Updating subdivisions (new P=16)...\n", - "Cycle 5/6: I=9, P=16, S=0.0025\n", - "Updating subdivisions (new P=32)...\n", - "Cycle 6/6: I=6, P=32, S=0.0013\n", - "Assembling output...\n", - "Done!\n" - ] - }, - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G078Pa9Hw7PnNv3qVRwO1i8q/KsSUfOQox7+Cmb8IjM/Dv9PoVIuGfMC/3tq5sp9cur9iDyqKHsGzv8jWnXa+waq/y5rzEfianL//aXcbSkdxvw9TECfs3pI/ls8ImPhRoj9Rx4wBh0iRP/DB0PmePYE/6Yf65a49lz+zLkmr1wmTPxZhqQTdHag/eLzyyMOfpj9zM64jf8OkP8uPH8trx6Q/AZ3MooUZsT/upz1NsEm+P0Xxle7Em8Y/an8DB89Wzj/ToOC2sR7TPxEue+UXLtc/M4v2DE4/2z95dKIjzVLfP9kokoL0s+E/CxAqPA+/4z8tNF3RhcrlP7i0Jcgi1uc//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j/EMRojggLpP+kip3tUJec/PQpyC7hJ5T8yl8JstG/jPwDyS27wl+E/Qbbknt+G3z/Lt9zdheTbP4hL4eCXR9g/FRuBowOw1D81LM1cKy7RP3W5xYk1pcs/yKkPSfGFxT/kpVGtIJ/AP+nLslWomsI/mbgsF0aVwD+5YxQHYMexP5LENz84w5k/cn1rgmnqj78DzeoNcsejv0ETOND4La2/5v+EhTtgt7/NiZ7yyxq+v57dyvlc1sG/gUj7d8tcxL8QLEtF66LGv2I5S0GnZ8i/DeJX9MYcyr/ZglbDAcnLv9JbkIDcc82/bNs/LXMez7+sclN8jGTQvwUY901LOtG/VOkvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UukvN+/G07+C3+4tgDXSv20u+jNuTtG/8GVKy31o0L94pPTVjQPPv7kbhWOaMc2/HWxOLbw/y7+6CT33hXTIv5ja2A5Zl8W/f+rWvZOXwr92zk7cgvi+v6AgfHgABri/YLV8i6t+sL9Q+HI96lyfv1h/kxdGZlK/f0oFQY6hlj+iJg3Crt+dP3aAochouaM/BPU4SvD1oz8uYovfU6emP1LAoFl6yZc/nErl+CG7qL9vjB8fmkS/v6sunriSDMm/QmskFp040b+UUii7CObVv4hBO67Hjdq/kasxmjkz37+pCFhOvOnhv7Nl/1aNN+S/TCmUoOyE5r9L42XT79Hov+jxtD2cHuu/AAAAAAAA8L8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7r91cyGq1Tjpv2Fw89rg8+a/E0NIbBCu5L9zYt3acmfivxHirYIlIOC/VuBDBqCy279EQmp50zjXv8e1tuzyxdK/kOFNbuWuzL9V59hjwtnDv44GeghuRLa/t7YUF9MxlL/qd+g+A3GnP6/myDU4gbw/7jiULpppvT/E06w9bfmwP86rGKP+7I8/ATWpxwWZoL+R2ReWvsOmvz2SHgnr+LK/GWLiZkoktb97+j2ljjW1v0YfP5nyqLS/PfiT0ifus79fFbH8uSSzvxznKYnXXbK/93W9v0Sfsb/Xp0XtIgywv9DH0AoYYqu/c1tD8Cyhpr+xRWj/A9ihv9f5Y02EEZq/B1wUMyamoTw=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+cuASuMx/hv1w1MABYWN+/u85wAxyp3L/pl7p7GxDav/5ZQU50nte/0Xfzi8A81b8pmb+X6gXTv2aBORB34NC/Awmm4XOWzb+4ZmIEx3vJv3DYqbXsdsW/XRh+O1SDwb8jMGDGLse6v71memeqJbK/reiXnxf1or/lfv5hTb5xv/ppMjqDqW2/B1RblS+foL9ZrcWKsP6uvy7p4k/iX7a/MpQ/DfUNvb+E5afuL7PBv9MzHPR+z8S/toa1NS7Nx7+X7JVj677Kv7SOoE1hlM2/dQwTAdMf0L9rbt3B2TvRv3yFxezfPtK/8FY09zz+0r8ZnDBN9prTv/UeNYUu29O/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T/ENra5LN/oP5J7SiWf0uc/QqaH5/Ok5j8s0amTsWnlP5X97G4tG+Q/NhURsJHE4j9rjoJzIFzhP/HqmQcx2N8/AgAGGfvq3D+1DIMND/bZP44qeac199Y/OWjIKXTx0z8QHdKJYf7QPw2nSsbySMw/8DE2fY+bxj/Gv7xA1NfAP+Z8nhT1S70/q5a5UwTZxD/ECK6BdxzLPwrsdVIzutA/qHemHNbq0z8h6BfzpiTXP7L7epy7YNo/fTXKAzyj3T+NmfAU2nPgP3p+h1X+GOI/RQWAB/XB4z+Vrx35z3XlP0jV1cUULec/jyH5q2Pt6D9ThKmzDrDqPyWDM8E4eew/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6T+q6jYCHkjlPxveJZS8teM/JJymH+se4j+049PdbILgP9b9PkULvN0/oFsi3Rdb2j8QQvdUeNPWP9U8kPyrBNM/+Zq+7/GOzT/2kPchIkzFPw6y+DGo5rs/hTWogZCKpj9Ql2bM6flxv9op9B9IFIG/x2T1WsGPoD8HfqzIn6uVPwTjyti80Z0/LwF1MRpvnz+f7XgqLxSpP08WJckDOKQ/uLfeZDTdfr8mqiWJNcqyv1cqU+K2r8K/4aa9ilLxy7+dIXaP+erSv+7+YJmZr9e/vkCJcLui279chfvQBLjfvxeMQTuXCeK/Sw4nqXxT5L+lR7hTzabmv2W1sMSj/ui/AAAAAAAA8L8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "YFp1BCPP4r+3Zy73wQrZv1UwlJYgUtW/s6fpzAqc0b/AmMek0tHLv5BLne1AdMS/NasKRPJJur/wYJsHXb+nv5oe/+zxHoE/Lv7UJBW2qz/YImQmvT+4P4rCblCrsbw/ctsFiyTFwD9QxGs4KT/DP4VV1S8RfcA/7orEBKf2uz8kndE2BB6yPwSNTQpuanM//Y3k7RZpob8vjQG+GjGSv9o4GsdFcYO/GrAARJCBhr8GTLtyneePvxwG0ejwfoK/BQJ/g7/dkr+c8KvI+QaevxJ5SqGO36u/2kqIXDIdtL+WseS99FS4v0Vehk/9nrW/4yaBSNCasr9rBIgj8vyuv3c8qezApqi/B1wUMyamoTw=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8L9Bw9oVTCXqv4EEfc3n8ee/3lXR002+5b8gCe64eYrjvxpJyApmVuG/+OGf3GhD3r+zOQ1r5NjZv7z1wOtCd9W/JM1/JW8X0b/tRZvho2fJv+MkG+uckcC/8+bY2Dmtrr8gv5iiJto/P1TrAJbcMmC/00PTj1FImz8kML/9GTGhP/QOFqLqWJ8/xbKPYpZgmT8x7MbD8/2aP4LSBmcp6Yo/zS4D6wu1mj+MQkcaB6SxP6h2LVnj9rw/JlEU9hg1xD+3QoOHRAXKP8tDAAb7AtA/aRvWGMTl0j97Z7hf9ZTVP20c2f9xJ9g/6YDrW2Ks2j9AaWeTeCndP8BzuOrun98/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "B1wUMyamoTxbW3kOe1yCPy8UVqgrzY0/4n3J/8KglD8m2f98T16aP9PlQrTtD6A/Uth9wdbuoj9KAv2Oe9ClPwPJ9T/OXKs/4qWPMZ7hsD/YZMmmcja0P5O6YQ9F07c/W9GR6QKSvD8oq/RolvnBP4SQAMWFksA/WWyi1jowvj868DvvLzmxP4Hh38OKbog/xZ5w741Jmr+OeoLv4wOavxJwaF0Je4u/SOC7u/Yfpz9CWotn+BS6P23kvsq878M/ErdL+fWwyj+ySsOH0bHQP+D1CbOXBdQ/sn+Bcm6W1z+h6+U6EkHbP1MN2qEH7t4/miohAVRN4T+3hM5KoiPjP5BPZ34Q+uQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+nL7GVVrvevzwr3UWiJdy/aB6+96CO2b/vfdhrx/PWvzhyKKqkUtS/BrMhOk6g0b/wAfBQ0ZHNv6xhd9gNVMe/cRqLIgIWwb+JgGQtWAu2vy6+nX5nmaS/sNm5qORcbz8cy+mxwWWKP51DIBJJWYM/7GJMIHlikD9w4zkI+X6jP4L9Vvk8lqU//wxpH+xlpz/F1IR1qQCgP7XvdEdAk5s/I6NRZoQUsz+quSLjHzLAP80BEVOkxsY/Kn0CHxJDzT8439M6mtjRPwzYzn9wL9U/YHkLNW2Y2D/87cxgtwDcP/gTJaNbat8/rIYrz3hq4T+ksIPoRB/jP0kFsh+O1OQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T8MVjmdAUblPyzeunzrquM/0MBOYsUP4j+wGaLzonPgP/K/X6gRr90/w4piKYt62j8MyI+AYGTXPybq+O+3k9Q/GPAEUEHd0T+37RWzZ1fOP9P7h0dr68g/oykZfCLpwz+uvXiy7zrBP77DiLYbZME/VTso5IoXwD9EGHShA86yPyaDUd+L85g/HiLA5cN0lr9294NoZ8uXv8yfZWW10Gy/WnhTU1i+pz8gO6xa4GK3P0IbLIIOT8E/vz7o2lndxj9MTFYOiljMP83kbRnDttA/iRgpNrcc0z/PdDi8D3/VP1BSCeyq4Nc/Zl0h7yNC2j/fX1l4aaLcP7QPeL77At8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j8GwHxwZv/oP5gx+bPA3eY/i0xBf+665D/2Qa8Jn5biP4tesSqccOA/LWAC31CQ3D8nvF+frzbYP3OzY2T9vdM/DILBNBZkzj/0SRqh0FPFPzgLiOf/kbk/xBWd6353pj9uduhbHJuLP0q9IB/k7Y0/ykIqyvqEkD/2F6nR812kP9+nU06UcaY/7qdnsE9Ooz8OpvBLL1OUPw+PC1zoY4s/IlyKTNRtlr8K2OP7REe3v0qyvFQP98S/b/C3q09Lzr99OL3p5J7TvxGbMCrG8te/ZTMBQ9NH3L+P/YoygVDgv7ol9MKvdOK/f/H3vw2N5L+zn2d4+Kvmv02oMRPjzOi/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z9Quc5b+gzNPyCTl+UO28k/rporzOuzxj8BboCUG5jDP1F3OFp3isA/U2cC/Pwhuz+pAXNryna1P6ESoprLIrQ/UFIst+Q0sz+DQaGtNIq2P1qgLd8QFr4/dBSay4NOwj/ye/eta9XAPzU11rzrJME/mT3dkopXwD+Q4B53+CqzPxbN1RZCdpY/mr9nhO/+kb9Ot0Huu+2dvy4g4E9jlHa/sNS8Z/0Ogb+7ZeraLXODv3qpJYF6Aoy/tr9ug1W/lb9fdPqUX2Wiv2ghp7AAZay/sK5/yB8ptb+WfrLSmx68v4M7IOZzlcG/MTroegMYxb9HLPTMRqPIv4/0vB5+aMy/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTz5kZG+UZaVPwHsIJ95vZ0/s30Jk0Xsoj/NyTVLQ/GmP52F/7NP6qo/QLa4H8kMrj/P+xOat2evPyj4648eW7A/TbGf8WjnsD9n+YvFxzqxP6OFBYQstrA/ffK4omJ9rT/O82LKbhKVP4CCR60Ov4A/rS3UT8uukT+5y+2dlT+eP69SsZSu4aU/sCBu5GsaqD8oArkNYpWtP6gnvKvA5b0/sgztm1Duxj8cTaMqrsTOPwttUhPHQ9M/8RQGIRMf1z/fUKP3SvTaPyIsOoAnw94/1bshLYVF4T9bOW8JBCLjP2GwEruE++Q/nGz93U3T5j/GyX3xhKnoPz1GSbkefuo//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D8zZm1cV9XrP+Rkpz4H4+k/voa59Tjw5z9nIFBLxvzlPzrqYQ6HCOQ/zzXvh3cS4j/SghkO8hngP/BAexrYQdw/yOmX+CVI2D9atBNtNUbUP1Q7eQ7TN9A/q+Y7dX1qyD/XvcX3vcvCP5KGwNigo8E/q2ojSdH4vj8bNvOsgcOzPzRr4oMxZI8/b7A+cMgPn79kAl/FmjuZv8h2yeQRdEy/TOL8gLN8kD+Y37UaVSSiP0Ac/a9ndqw/77jB40Klsz/PiAhC7Si5P3PanATK0r4/WSKSDFY4wj/gTNHQRS3FP7Rx0rD2J8g/wdH75+4gyz8L8cGXcBfOP2zBDqfphNA/T+kvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D9gEiaLihPpPyyUvF67tuY/zZzp0d1e5D9/r0lEngviP9bsmVPudd8/HxJz9EsA2z9Q0SBWEbfWP1DZiD2AntI/eXMCtGZqzT/vSG8TjFXFP3e9a/K0Brg/Sm3E+jKAmz+AOpcLpo5vvwgv7ufVD0M/4FLPCSjUbz+yl4fixf6lPyYvca0Jsak/Vh3n26SGnj/PRJe+li+bPzAZXvCHuo0/gArIFtCZkr+TOXdnE/K0v8QU/u5FKMO/zsh3BSKzzL8bzMQmOR3Tv6TfGr+inNe/ExvMksLz27/BIJto5Qngv6RDUowrKeK/lsSQFpVO5L9Ignh1PnPmv56ZJjXPmui/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAACpFZnFoiVrP4bq169sboA/i7Dsf0+Gij/wuT1mv9SRP4c3Lo2y95Y/M7PmTyvamz+gr1n6O46gP74/aggR/ac/OOxwpKr4sD+UMsixN6K1P7mg2H0947s/N9a2oDJswT8IshpYrBLDP0L2+phKssM//lD3PWLIvj+YJ95efvq0P2AlZZ+LDF8/IB2wkZExgL/AStaDBnmAv6z94LW3Zo+/deYEQhiQiL9N2ebm0WWLvz0U9iR46YW/YRF56Jb8h7/NBtKDV46fv97YnSpbxKi/lMBWh1tqs78h+aVIRR66v+e6Gu0P3sC/CTiAcJKcxL/ceHCJOEvIv6VQTZz1/cu/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AFVEEw5v7r9Fd3welybpv72SDosNIee/EZGn03Ma5b9+M16ocxLjv7pueraZCOG/DgNgOon43b/ONF2/p9fZv0Ltl0ajntW/m2YHb9pT0b9dwFSdTuHJvzYwOTKz48C/Ig6EGi0Fr7+8RdA7Oat9PyLLMgCZEXw/wLpUEDK3kj+zfb4ctFacP9gzT/6S8KU/nwwxOa+Moz9Eg7zgqIalP97Q8aYDBJk/73uTLhxlpz/DWIdunWyzPx0yWUP6Vbo/lTrKs8JrwD+c6e1v+3LDP0hA1CXrbsY/xCzjzmWmyD+600ORd83KP6NhEsGt6cw/YT2Y8xX+zj/GKgSTRobQP37kVo49i9E/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "TekvN+/G079/of5HNo7Mv3puQJgTeci/v1qndIljxL9J8B0Js03Av26OPFUzb7i/if1oVHdAsL/wZolrbSKgvwD/vA6xwFS/4JriapXmnD9qVqsF0i2tPy8u/KY8gLU/5tODE/cpvD8uCZW0D6fBP4AbsVnKfME/goYV6P7YvD+xeazXTVqyP4lpzwIEfoA/ZrIcY66Vk7+GGNPh2AGZv34E+i8XE4q/ofNnWMNWqr++puuWSUi7v8L9i9M6gsW/qi87+E+9zb8nwN4wRQ/Tv2rhXUahQde/quaCID+X279yIRoZJ+3fvz8ZMyk0IuK/AejtVERO5L9HO6TzC3rmv5OVzVDmpOi/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7r+1j6nJ2WnovwT1ysZ/Mea/XSLiI3D347/Bm0mTULvhv7MzvxL2+N6/pJfi6yx02r+IdI13I+TVv9s4OhLrZNG/RmbZ0Krpyb9UaspXlSXBv+JoFctMZrG/yAQ65moChb+Id0izKCGBP/St0SKfVIE/paxDoPnomT/S3TYZi6+XP0KrxfGoUKk/9fSLiZxzpz8rUexYycujPxAfX7jSh6Y/HNamvYi4qT+oGVEw+2yqP/wiCVT7MKk/WzHXgTjwpz8yt2VxvbmmP7W+Emok7aU/4k10rKM6pD/U17tvE7ehP7IGWupwY54/fs5Q/RIPmj9zaaZUdmSUP5mNDz1XQZA/CoqeTDl5qrw=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "UekvN+/G0z8Mq+hUO5bUPy3txp2IhNQ/IcEBM4Bs1D8dMUst2EvUP2pRleHsHtQ/OGFIX8rf0z/Yy65YuIPTPwBgN0VwnNI/7gNijTNF0T+D3/2a9krPP8l1U74fTcs/2Bg0a+3wxj9INvBQm93CP6STYfHSEsI/y905SSEauz+t1bDIXsazPw6q7gHGsYk/+owLme2co781RsMo+najv5POqhgvx7O/6KUf7jWiwL+pHCISxDbIv5KjYQReIdC/5oKcufg61L8CBEn6x1XYvxcO/Frwdty/yvwpz/pI4L/Z238NU17ivwJkgsEgeuS/hCvVzLKG5r/7DPJXEKnovz6yZBhwvOq/AAAAAAAA8L8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6T9J6X8WzzTlPxyNQwPHn+M/kXI7B3MH4j8UjCXhCmrgP2v5sCYwit0/JQsHu1op2j/nc4sx+qTWP+vBMPYK4dI/AOVA/gx8zT8D40t1VEnFP5ZywV6QNLo/EtpNwJy4oj9oq/bPMeRnv14mNH3ASIm/ZrnfkZG+oj9EOFutWjakP/xIsV88YKA/hRdvq+eOpT9C9D6RyqeoP039DVkkg40/KJ1RS+5Oir80BmejbAu1v4AM5sgOAMO/sfcdhYVezL/TTRKPmvjSv1bED2Embde/2dNO1wKT279mjzNW++Tfv5X/yOx2JOK/uF7oq+xR5L+2kFVa9XTmv0ugIWyxmui/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "YFp1BCPP4r93GRZc5AnZv8rGbJX/UdW/VZyWIfmc0b89FqLbUdbLv4KRKBP7e8S/suc22ylhur824o/RGf6nv1zCzqE/BoA/womPAKLIqz/eUr8Cgdq3P67iFiY1vro/1AaTUfpxwT8P44wt+Y7DP1UrnJIBvcE/9PmLdcWvvD9IoactpFauPyA1Mcgf7Ui/cZSS+SaUpL/6ZQahvqWCv2A5i6ZLQU0/qOsf/Ow1br84NWvnDzxwvw7K3Lo5XHS/rmiB+10skL+2Rf6Je4Gav/aIJWmvVau/oK0uYN2NtL81+95wm4a5vw1ZkmPChcC/TfGR19ZBxL9lu5D5tNjHv22vkaVrm8u/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j8gG/7OkzHsPzy/QQ93xuo/bLiHcGxY6T++ijEaK9/nP6pVTplPNeY/ZlfKNcEv5D9RQXmtRxniP+hAvoR+yN8/bQKJnWUQ2z/W2VAjXFbWP0E2INlnzNE/UFQ02/7cyj8QSiimsB/DP/uum0eIQbU/+BcNGqhnoT/PflOWizajPyvllNpIaaY/Lqf9+kLOqz/T3mtVlqq8P4+REZ6P68Y/8pFL3KtSzz+zB3ITQybUP88r2f+siNg/UX0H9u7N3D+CvA0apmTgP3KeMobMIuI/E8HUw66z4z9wZAl3byblP/WBFRZPVuY/n4fPrrE45z8HOGSnSQ/oP7MQEOXl4eg/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G07+F5U3cWtXGv05VR8xXRb6/ZSKWUjezrb+gy4onsaNePwBL32v2Na0/f/Sx7nDNuT/5kyC07+PBP3BexOjNM8Y/QJdXHz4ayT/tD70rOgLKP0NKvrAq2Mg/8Gw9k23Wxj+W/k7ASsnEPwLZyR17mMI/ncjqbnLNvz+vVLVZXP6zP6o8hBbwOHU/t5LARQIKnL8r2Y1rLU+qv0Di5FH9g7C/SdjRiX5Js7+25TY+2P2zv3A+r3vKrrK/ZCglozIopb9ADGrVIwokvyZxA7qqNKw/hDPhcDggvT8X7LqeLE3GP8nptbzqqc4/z2LrejXd0z8drEop5WXYP73Zce1g8Nw/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z/yV6zZi07RP2tSx6T5SdA/iEaeU+OXzj92vPJRrKTMP6WxP/xmuMo/FzhitFTYyD+XvwLjPMHGP+3PKyjHjsQ/3qyp3Qstwj/yjdPC36S+P736a4YXt7Y/eoJX5pTyqz8yrQbdb1aTP0K4oOJLH4I/CJm6dV82iT9ivYpqmwKoP3hKQ0ldG50/si8N9sVIoT8Mw/D7fLipP8UXkIPAr64/HFSKT1hGuj8zwKCSUzrDPy7BwEuCbMk/zLqPg42rzz9IzpU9Y/3SP9z44VivLNY//FFgALB52T+PcT8fDejcP5cIm8UtLeA/1nxTxbfn4T8ymffgYKTjP/NoP4zZY+U/p/SXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j/aAoKKI5DpP8Rk8dcbo+c/vy9HBoK45T9ICnvJ/dDjP1SMpJjM7eE/JK7sT3oS4D8mz0rFO4DcP3hADYvR7tg/Jf2lwFZ71T8BuRSx3ELSP2SueEjOiM4/US9elwXoxz+nxP77QHPDP6+Jxugz3ME/DKEFx8YJvD9WWKgLPHmxPxBx9VLRKYE/5NWuVfH5mL8jMAw9B+OnvwVtGXHsnra/kdts2EDlwL9ZFDmpD57Fv16twzTQI8q/8RgFauqozr8R0QoRFKHRv/MP2X3p89O/vUC40N4h1r8Mw2UgVDHYv/MChCe7RNq/NXLi3VBb3L+MFM6ZCHbevyFfT197S+C/YFp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G079GyrNGLb/Uvz6rN1Zrt9W/Noy7Zamv1r8vbT9156fXvydOw4QloNi/Hy9HlGOY2b8XEMujoZDavxDxTrPfiNu/CNLSwh2B3L8Bs1bSW3ndv/qT2uGZcd6/8nRe8ddp37/1KnEACzHgv3EbMwgqreC/7Qv1D0kp4b9p/LYXaKXhv+XseB+HIeK/Yt06J6ad4r/ezfwuxRnjv1q+vjbkleO/1q6APgMS5L9Sn0JGIo7kv86PBE5BCuW/SoDGVWCG5b/GcIhdfwLmv0NhSmWefua/v1EMbb365r87Qs503Hbnv7cykHz78ue/MyNShBpv6L+vExSMOevovysE1pNYZ+m/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j+mRRcq4BTuP0w26kCyuu0/8ia9V4Rg7T+XF5BuVgbtPz0IY4UorOw/4/g1nPpR7D+J6QizzPfrPy7a28menes/1Mqu4HBD6z96u4H3QunqPyCsVA4Vj+o/xpwnJec06j9sjfo7udrpPxJ+zVKLgOk/uG6gaV0m6T9dX3OAL8zoPwNQRpcBcug/qUAZrtMX6D9PMezEpb3nP/Qhv9t3Y+c/mhKS8kkJ5z9AA2UJHK/mP+bzNyDuVOY/i+QKN8D65T8x1d1NkqDlP9fFsGRkRuU/fbaDezbs5D8ip1aSCJLkP8iXKanaN+Q/boj8v6zd4z8Uec/WfoPjP7lpou1QKeM/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G078ayj1JTXzNv+vjVOQVC8q/C5rR00qZxr/V0J2P/CjDvwhsCmDbeL+/1DiiowKsuL9zuAPKpPCxv02LX13dxaa/ex4TTn/0k79U6XPon9tyPzss0BdEVps/Xi44cfVWoj8imV/mzk2cP6DK677UK5U/WkCLwQdliz+IcGCg4kqdP5WvEisic6s/GX5wTp2krj+Eq0D/gZKmP0v2SZ8VdqY/PnCldKfuqj/awi0tcISyP5+7IlqS17k/JiA3vsnswD/fXawIuBHFP1lBOUZq/ck/xcbpiWM4zz9nggFFH0TSP1bjIbCm8tQ/UGX6e6+k1z+V6IjneFjaP70oJaAXDN0/XFp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j8qMxnUswHpP70X9gnDJec/HFzIm2pL5T8+5xWzEnPjPz/dbEmAneE/LWCg/1mX3z9rAErpBPnbP7QRKUyBYdg/tsZbQBrR1D/RC3Yj+FjRPxqKKjBUE8w/gpsOyeLxxT/rdah3CSPBP/jEH6tpj8I/XqbI/N6dwD+SMkimxQu2PzlTmv5WcZY/C+BkHJtymr+/J7ACSUWpvywF//nOCrS/ktmaS1F4wL9LJB6NIuvGv86g2/6Gcs2/Xwt/DjcK0r8rC8C6r1bVvyVW4XFcati/eopKuFZk279R8gsGrl3ev6a6y1Pdr+C/BIDlajc04r8+x46Md7vjv3PuFBusReW/qfSXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "Xlp1BCPP4j+vmcfgpKneP0TZW8N/39s/GOQ0X50V2T9ySRCzOUzWP8M6nDVwg9M/0cnHYkW70D/0HT2Bxt3LP2srIuaxUsY/X/kxhB4hwT/e6EKu4U24Py5umUL9l64/rqX1TCcCnT/cvGvHSgJXv1WxwZf32X4/GbiR3ilbmD9kGAKnmWOeP59uLLhJcKE/XqqXp6+goz/jnqcTzFedP0gQ/sqpZZa/BORO9k8PtL9Cy6E9HunAvxBiUhc4Oce/5KQglJhAzb9Ll+6b8XzRv7tq4hiCRtS/BlFmmVt61r9ZUVe8pJ7YvxTtddczu9q/9SsDuNvQ3L8c8iQ2K+Lev+2hxQwYeOC/X1p1BCPP4r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T9fANiO5vXlPydpVxt7bOQ/Xzt+uizk4j+4BuM/sl3hP74cK4PLtN8/C7BK24C53D+ylm3mbMjZP9qVwUDi4tY/roXkXan+0z/uUXGm5gTRP2U/H6QI0ss/n95Y/Q/TxT84Pnl6htfBP+kxcA5+ecA/snnOIRKJvj/w886j1UKuPyZzpKyR3JM/zr1hioAunr+27SqNuoyiv68vt0J2Vqe/eZxHapkJs78xaxhce469vyXgpy8hysS/SQi/JS8Cy7/vI3glSbXQv7qaex7f7dO/693Y+xOT178t6QjcNT7bvzSC3B1T6t6/2pfVcThL4b/Xok3eKCHjv2aw3Rr39uS/p/SXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XFp1BCPP4j9AUGfMBMffP6gIiOVcy90/knCyvmPM2z+xvaQgCsjZP3wDxzuXu9c/nMyx83ei1T+sFc0U9nPTP5MJXkSZGdE/AyvOlTynzD8E5tnO487EP5zP0g5+mrk/ozK1cNKCpT8jyWRpo7CKPzx1L7vcipk/oFAzbJDZUz+nE4FXdi+oP8DHMwQTyZY/otvJAaHCrj+HRl1TRXKgP32R/2dybaA/nMEnhekYYL/MtGhqI82wv16KAvdBgsC/+tzpfwe7x7+MtbbtwwfPv8aZcjbB6dK/H3+R/NNj1r/S6mbDWfXZv8pq+urbkt2/KNfEWsma4L8nwK0Fg2ziv1FUIxZiNuS/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qfSXm3fj6b/81AHLB3zhv1kmeXNNGN6/KKLdW/A62b/VFIBFsWDUv8Ia+OwmFc+/CViu01h0xb+QDJRbOs+3vyxZvPRgtZO/PC9HI+bVqT8amOMOPmm4PxeDVZVJ870/sPqJOZYewj/qQRkThPTCP9X4vf0rlcA/UhYmEkYLwD/aleEtlfquPyjd/eXowpg/enGaxqgDjb/UKKOnJ9akv1mxqiCkcaG/kVQ3K3r3nr+rcxRqVruavyQlZueojJa/BrcoEFsDdL+mrepyGQ6cP3DUfNPq2rI/tw3IYtq1vz+dVdXKR5zGP2N5S/X0js0/70EHQ9lQ0j/NkqnRyeXVP24S9/AuXdk/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j9bYG8L8LHrP3hk8AGDv+k/NcBdqSXO5z931RspDevlPxKeoLBDCuQ/z1mQ+2Ar4j9qC6ezC1DgPyZ1Faa+79w/1nhbQwqA2T8STSjTQLTVP/hNyGy4x9E/bdXtKWShyz9D+lGsmXXDP+onewmijbY/jK6cNIB8oj+vB8KW4wmkP5rGAlnQK6U/QnPjPyIFqj9+4hHh45a8PwpJwXQeWcY/mV8ddEIuzz9AeoNUrI7TP20fmopVp9c/T6aLZ6aX2z+ZP7Q/hxrfP8F1VQyRRuE/53UCLeUA4z8xF4vQe73kP3Gdxv3FeuY/wr1EGCs86D9SmZpodgXqP/ToJy3e0Os//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z+iVQaIYNTTP+8G0vpif9M/a/gIW9op0z/ZzYMQ21bSP/z4CxAXZdE/Ogu32YBl0D/i0wQNAs7OP8F1s0K09cw/f0YtxArXyj9IQT6zkszJPw5e+OEqp8g/BFMf6oboxj8IDt4KDY7EP6KZWSlVuMI/l+QAQHeovj+BTjyunvuyP7AzoPqi84k/VU3GwH/voL8wdlcx7yisv6jt1mUfQrG/cM5uTG1dtL+SA4BZ98i2v000scX407a/iI2udxmdtb824TwPyiS5vzho4dzC6b+/a1XFLyV1w79TzQRrVfbGvwWlLX8Nc8q/u9SNWM3Dzb9vobN1lGDQv4ccPA934dG/VOkvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UukvN+/G07/A45WkfzvSv6hLDT+sZtG/pzvrlj+Q0L812rJCu2zPvyoJxmSZrc2/VSEQVXXcy78uAwyBM+zJv08qOtLvbMe/bLir7IARxL+cMJpV4GjAv15MPe9YWri/o53LnR5Eq7/gCqNf0Xxgv5S+M4AIrYc/mFMYdIVSoz8xWAxr32uiPyePTbyw8KY/rHNF7bRBoD/W8RuPx0afP2dag3bJJpk/XsTk2raocb9G4ltg+x+fv7ZAe9mPE62/QXjkRqIttb+8Wypwo7y7v0G72fXe3cC/khltPXWTw7/JnWAGMUjGv9wTtpacF8m/KemFjZivy7+imuy9eVzOv4uuWj+db9C/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7r+NnTAyN2bnv28n/ifQ8+S/8WxJn++A4r+IezVTHA7gv5j4nfIhN9u/JUfuhZdS1r8RnDr5SW/Rv3uC/15DMsm/1JlKL0Klv7+AwOtvkBCqv0lr6t73r5Q/9/mjcotXtT8vK/ubXX3BPw2A+kMXusE/3jf0pyo4vD+BbLmvRzm0PwAPYgBDpj4/wIg30UqSor8IXYyry5Cqv2mbpYExs7K/9ZfScH8yob+iG5ZHp7GbP5Cjafup27c/nPsDBFG3xD+MM4HvJKXNPxecD4MKWtM/cXyQfDLy1z+aggjwBJzcP9lNX5tjkuA/TZmJ1jrw4j8sPHiyrEnlP4ApX0pinOc/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AFVEEw5v7r82r5/BrBjrv4FMMp6NHum/bplb7SQk57/2G/hEAinlvwlP76Z0MeO/qADE+9c54b+WHbR2SX7ev7wCfePWgdq/ohly62N21r9FDNoRGFbSv+I+UgmIF8y/AxBgC5kTw7/EGG64leGyv8AQYvtsFUm/MpfLkPIPmz/G6gd2XyOiPwyVXO3MV6I/EleSNDgGoD9XVxnq0Iagv0iW2mJbira/uAFKL44rwb/KIs5BrUbFv7mK/5XSVsm/ikyeY6U6zb8Z1RwlZpTQvwr5PPnXotK/l6+Zzpe/1L9JKKzmEezWv1CGLxKrJ9m/xfbPv+e+27/564Zbj1nev5zOsvINe+C/XVp1BCPP4r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "TekvN+/G07+1vZ5tAh3Qv83FmMBwt8u/2IRjVXY1x7+efzWWt7TCv/8byZLUK7y/9a+A1BXksr8Vv3cEmmmjvw1bivPwBWO/tFzMm3GioD+6oWeMcfmwP0oNDAyoxLg/kiLMhq5Zvz+GEo+iNWLBP+WZ+9hUmME/aMiViM+Cvj9PaokMUtKvP0GS9bfX0IY/UE06+e0knL9hEXm2cf2ev4cQLVVwsHa/KCVG1eiRpT8TVkYjWZy6P93vKvsVcMU/W/GdHBCmzT+KzcCVXeXSP5blRYIv69Y/Kb9JC+ju2j9/qpqhUuzeP+kWaY6DcOE/jgnGO1tO4z95p0IfMSzlP1imszTaCec/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UukvN+/G07+yHPi6DvzRv4QkCMDsHNG/gzWbQ6440L/o0GDZVp7Ov3DTTXdLv8y/BaroKDbQyr9yB/LJ2rXIv+sx2U740MW/Ag6pt4TDwr9ue7jGxfS+v2kgE9/dz7e/aHnG8kHArb+ihMYfz1qCvx5YLcZuSGc/sFAFG3vRmz9T10sz5g2hP3NmwPVrkaA/vpXoF2PSoT+cFmfEWiudP3z5CoZdKGk/UjzfuKPGdr/Mk6dlGsCdP+WjmNi5KbM/q1gGcEYSwD+k1PdKmdnGP66/1FyIuM0/IAyb3Bqn0j/kw94XocHWP27fVUTs9No/dhmtRbQ43z9imvG5lcXhPx6sqkmD8eM/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7r8L9NAsMyLovzRtFZpFuOW/rljMfuFO478e6XPCiOXgv7HBfVZa+Ny/B5b9YP0l2L/B/WbrWlnTv8vZlSCXTM2/1LS3xVDTw7+qB67czbe0vzDN8ZWoeHq/3aY/lDVqsT9mUw8J/D7BP+LLbk82pME/hC50IfAFvD/wUlJbEO+uP7C+v7OX0oI/3/vRE1Ufob+ot52OJbCiv3G1GDYNbpe/GlJbDONKiD/4mk8qWc2yP4nCeV3aAsE/MfVHhf5byD9AXwxwOFvPP2Z1XvGj3tI/uaD+Pp1S1T8hKzVtcJPXPygP0TmOzNk/jjzKbAsE3D/sAybvMz7eP0D35gu1POA/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "X1p1BCPP4r+6abQLPuDgvzs6ddK5Y9+/r/2V9qsF3b+0nmaJ+aXav7MlM4hANNi/Ii3oe3e01b+G1V6vMDPTvwgv6yT9rdC/VyaSmA9PzL8Xdk1+uz/HvxHFAlE4OsK/wKg8APeOur9Q6l0qEOmwv2rDbnuNzKC/slCMsQ6Ihz+kep8nmr+dP+Dj9T6KoqM/qwfA+++koT/atvkYORaiP4bTn81nNZ4/1RY44egSpz81yIlzys6wP27+A/iPR7Y/48xr8pX+uz8h2GgBw+HAPwfq3FoH08M/manOdbbExj/z6WhpUbbJP/0Ymp3YdMw/Rmp5lSvuzj/NM9uhgbLQPwEpG7ER7dE/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "p/SXm3fj6b+rSvPqE53mv5CeuLnJvOS/kFY1a8Db4r/TAVmG1fngv4YmBD7VOd6/BykG3TCH2r9FO+eZDdHWv6IwgK7+FtO/o032mTe0zr/ghoXGzDHHv3gkFHnGOb+/wGr5ozrNr784SUYsv95hv+XPAYHPE64/qSX/ezh+uz+4IpqvOKqwP4mNAdLRZ5I/YJfNWLP5kL+jngbXJAOVv2/Wa4WskKO/ItOR5spFu7/954ebKT/Gv8JNkjlSzM6/2M95puui079pRheqEtzXv5K8+IdsEdy/ii+Do7oh4L+6pNqE1Djiv4Ko+q1/U+S/uOd7VEdz5r8repvWL5Lov+xBXWxlsOq/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "Xlp1BCPP4j8MBzpPp6bePzlMGV6J69s/KXNi+bQr2T8BEMiEo3PWPzGNqRfeyNM/oj0IOV0S0T97Abvk+IHNP7noOgolrMg/Cincg1ZcxD/OF0cR+QbAP6oN282tebY/EKXxMJFIrT/ccDsFTN6UPzoEQo3kJI4/O55jyUrsoj8JaJ4HCseaP6ZWW8z8ZKA/Toym7ZYAoz9TAIpbQtqlP96W9I62FLA/CfAUvrezuj8Ty9YJk7DCP5RdBzpousc/izy2HWD6yz8749DagxrQP6Ec19CiN9I/2HRsLZFh1D9sP4Gv4J7WP5JM+fVn39g/YOtMTsMg2z/f6E2Qr2XdP3K8cBOesd8/XFp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T8agoO1z53mP5H8xP+OOOU/aX+GqC/Q4z97DKsfYmjiP3wD2iAJ/OA/f0Thh4Yt3z9QvTE0LRHcPzE8EMKo79g/gxoA5i6u1T9SZSq7PmbSP6972AMZas4/5jmGlH9LyD9/MaAfulfCP3y6cTRU0sA/kIYaETvevj+/5orgQ2SwPzbdlVTOJZM/xTcJgia6or+8i1w5zcGrv+Da9NWaZri/CPNiwSjhwr9fZjqJ5TPJvziNA/xHTc+/RmbtWibW0r/BvC4hDhfWvyeI4+97Vdm/78mXUNmB3L91T5A3OJ/fv/XX1BhkX+G/yCA7Ypzu4r9D+YAsn3zkv3TfE+w4COa/qfSXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+K9UD60rnfv3nwqqSML92/45aG2Sym2r/x/DReYBzYv+keSJlgkNW/DBIVyh3+0r8hJOrLH0rQv6d5vxsGoMq/CICdb7eVxL93LkH0VxW9v25eXHLPVrG/+ANKoNw1l79wHnMDfgRxP7CbSEBxsFw/0mntfGxJlj+Gg00B7hCgP3zBqjflAKE/hqLN/abXoD8QFdgdZsOgP+pNoBN6waw/FPNDx72/uD+QYj9sEBXCP2fwiN0rucc/XImJC8s9zT+liWEGGVLRPyWqDc2S+dM/Uv6Ug7l+1j8NXOhWD7nYP6Z0KpO659o/WEhh6/4O3T8Cdpi5hjHfPyc+QxIBqeA/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T8iRP3CSt3lP1yAvk1uRuQ/JFLURmGv4j/OVTfK0xfhP7DplT7x/94/Zd9dB03Q2z+2q5DJoK7YP9g0TdpU0dU/qwZ7lyYK0z+KyilsGU3QP27txCNJKcs/rLL8slXoxT9SCMp7benBPwCA7cJ6H8E/7gbYuTwdvj8s+uv0yhSxPyaNxuysoow/o+bxNvX5lL/yyfWkfROMv0wjlXlZQac/l5N7lZIruT/kywxXQhHDP40nQlKWjck/QEEpAX0H0D9aDjYjHkrTP98ZkV48j9Y/OuHQ/cLp2T9sA2YZ4m/dPxTOHYiTeuA/KDTz5Io84j+3OxzCgv3jP4ToncNVveU/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j/OXb2VOmbqP7HzuAIoWOg/GL8VY1NJ5j/w6nIKmznkP+Ndy27oKOI/LKykI1IP4D+rcrB3QtzbP/JoOeA3mNc/fNoIeOpL0z8z8NEzq9bNP/KZ/b3u3sQ/KLMS7ywPtz+Wqw6x7piVP/F5WpfhSYc/wpxBbckEkD9K4Pv3PDqgP+UfeJgwYKY/8WLRg++YqD8jOtrADOSpP4rmukBqPLE/EyixlBzqsj9fQzPNio+yP00O1wNtm7E/jPFyl99DsD9aLwWatp+tP8mxEbOuwqo/2TW6qTrXpz9kbQndmIikPwaCjItrGqE/IdVAzfBZmz8pJdP3UoKUP1krDN5QXYs/B1wUMyamkTw=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z82QhevK7HQPyw34Yx7Dc8/KmX/Yva7zD+wfYJhU2/KPwCxSbCMKsg/wA8TVLu0xj9ajLf4Ou3FP27VIW9RScU/nS0k4xTcxD/lVccp8p/EP0UtyWn/vMQ/A2s9EbXBxD8p/ZBpXbnCP55QtCSj1ME/5LVFOTB+vT/vdXbV8bCzP45OjhqDZYs/9MvM6S8qnb+BU5wbh8mWv2j5h/wxmaQ/BxekA9E3vD84Aw9qMx7HPz2QjL8RANA/swGr3dJn1D+UxdznvcjYP5lV/epJJd0/A5NcWn2+4D/+OEVQ9ufiP0e4stu5D+U/CRgoCT425z9hUwxusFvpP64tpDg4gOs/AAAAAAAA8D8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7r8MCQsHW8fov2w2iaP1mua/017v8phu5L/fpeHdRkLivx+LkGMMFuC/o733wT/T27/4vFWO/HnXv3Kv18ePLdO/h/kPrGrIzb/N3IHdfTvFvwWcTCbodbm/GhNz61+pob+wh4EFC2cyPx/ed1narW4/UpDPCmEKkj90LwhaqTGgP+cVwLAKXJw//smH9hHRlT/KjXV3fU2eP1St75rsQZA/lihxnoeWqD+/PEVl7gK4P+zG+ZZWqME/1uonEGIyxz9mqhPkq63MP6ZnMgW/IdE/h/L9/h/A0z/brchtzkTWP8d5rOBEudg/DW5drsgk2z9c5nHYxIvdP+GzRTGK8t8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "UekvN+/G0z+/BrEd6HHSP+a987XyztE/7olrezQq0T/V7uUcJ4PQPxXRu4Dzsc8/vEiD5ilTzj8uRAIbt+PMP7KkwWMNDMs/MthZW/ktyT+FMVutQUPHP0C49RAfScU/yQ36uhpUwz94z8ZO41vCP7z9pR1S5b8/FsAvfrUUvD+uz9oXGLGuP66Ay+BY2ZA/1AcFFm48nL9wOJLqUAuXv2iR6sS2+Xa/nnkg9cHwqz8EeNpkqrK7P0aYpyRwmcQ/1WR/sMVMyz+WeQUjWv3QP/DSHO/hTNQ/IJMLJ//O1z9n8BLkv2zbP0BQJJ1sEd8/sSHLUPJb4T/WM8rxxC/jP8n5dliZBOU/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XFp1BCPP4j8lZcZhVJLgPys5HM8cOd8/CEjcUpBE3T8kLsiJuETbP/wlST+yNdk/Zaa6zBoR1z9rrbkKxMzUP6lTGTsJVNI/aNLSOwRDzj9Q/dxKTh7HPwdJdL2bXr4/7FvX4OG6qj+4aSj2O32GPyR+GfwLrIU/NoYnElILjz9Vrj6tbaShPy+Ils6zMKE/H5ESu1Yooz8E+33OdiGjP6zhJ8O+PKA/rj2XpXeLhL8mDNTZE0Gzv3nfIMbfGMO/wfNKm3Q8zL+EQMWLnc/Sv6RwrwM3Xte/sPuIpieB278rp1GiBszfv+0ybEwmHeK/0pRTMzA65L+T1yojCn3mv0J/WyQGpei/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qfSXm3fj6b/t+d6QPwTjvwcbTEbKw+C/o+2hiYoG3b8KMHs7MYXYvxuxhcLhA9S/EqtlAwIHz79uyYB3LwzGv/cvuPoWQLq/qcv7AreapL/aCwaJS6eTPyWKW1pf6rI/9twL5xxEvz9ExdN426zBP7GOyUDdI8A/cxUt4E6IvT865Y/Z4DC0P+QVyszrp5I/IvJbHN5am79Y0+0GwxyTv0wZKXOzcX+/NeIKUtOSgb+aZbZOerNxv4ndivUFDI2/RykI8jwElr8oUPxhy1Wev8zzmbXw0Ki/RqaLEkRks78qN1442gO6v2K56lQJtMC/qsHBVCtmxL+vq3IboZDIv9toCtC+Tcy/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j/y4kjmDbToP79Jq2wNi+Y/77xKZWBi5D920GxO8jniP0pInYq1EOA/v+jCQuHJ2z8ctFjcDWbXPxbpK1w21NI/XQzIlOaMzT9d+/ZyfgPGP2ksl00o77o/fce/f3vWpz8aK3IjA3B/P6T3iCsZ4FU/Qq7ds43xjz/Z/0KtDk6XP2qfVyXcIao/W+JzD7AGpz+6n3PsitejP7InQSyS858/Cqj3v4ZNiL8ILx/i6kC0vy7Ok5uv/sK/PWQPAzV+y7+dMbKMtgPSv+geLwYaRta/3t3+maGS2r/sCCwjq+bev1N/TN3MneG/m8rhddbH47/HS47VIfHlv2SMTQQgGei//1REEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z9QRnIfLk7LP8YQIWdj38c/7sQxeR5wxD+97R/yDwfBPwLuPL9sVLs/nJsCZbTGtD/KwwPvpiWtP+Vsgo9dZKc/owZWearTrj853q1r+Rm3P8ZA/AplBL4/wiPTHzilwD+yrbNnj/K/PzFUjBba3MI/lll8yNh3uj9+4htLvl6wP4eVP7DTHIw/RgYZ4tydjr9bo4Y43yChv+KSzIMV2Hq/q/BBI1hAgb8UQgWffVhqv5BuOoVoFkK/wB0LGHUymT/OJl7uxP2qPyizN7Rh0rQ/ua4ghBWruz/jX3DlPyHBP1IHE03CXcQ/xHykiDWUxz9V3jZMTcnKP5oOKuD+/80/UekvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "Xlp1BCPP4j+vmcfgpKneP0TZW8N/39s/GOQ0X50V2T9ySRCzOUzWP8M6nDVwg9M/0cnHYkW70D/0HT2Bxt3LP2srIuaxUsY/X/kxhB4hwT/e6EKu4U24Py5umUL9l64/rqX1TCcCnT/cvGvHSgJXv1WxwZf32X4/GbiR3ilbmD9kGAKnmWOeP59uLLhJcKE/XqqXp6+goz/jnqcTzFedP0gQ/sqpZZa/BORO9k8PtL9Cy6E9HunAvxBiUhc4Oce/5KQglJhAzb9Ll+6b8XzRv7tq4hiCRtS/BlFmmVt61r9ZUVe8pJ7YvxTtddczu9q/9SsDuNvQ3L8c8iQ2K+Lev+2hxQwYeOC/X1p1BCPP4r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T9fANiO5vXlPydpVxt7bOQ/Xzt+uizk4j+4BuM/sl3hP74cK4PLtN8/C7BK24C53D+ylm3mbMjZP9qVwUDi4tY/roXkXan+0z/uUXGm5gTRP2U/H6QI0ss/n95Y/Q/TxT84Pnl6htfBP+kxcA5+ecA/snnOIRKJvj/w886j1UKuPyZzpKyR3JM/zr1hioAunr+27SqNuoyiv68vt0J2Vqe/eZxHapkJs78xaxhce469vyXgpy8hysS/SQi/JS8Cy7/vI3glSbXQv7qaex7f7dO/693Y+xOT178t6QjcNT7bvzSC3B1T6t6/2pfVcThL4b/Xok3eKCHjv2aw3Rr39uS/p/SXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "CoqeTDl5qry5FvPc8fCiv15qhqKG766/Q+d99KORs79WeaINlwe2v/cRibpP3Le/CGRdMc0pub/IucmWGqK4v758F3tvq7a/nP2L8yx8tL9qgSCIhRqyvx5NuDgJAa+/oMGwjQxaqb/l9R4Bkaqiv89vvxUle5a/ypEPXEEwbL9+h1+Ru3OUP+YU2JPe06E/2BgeyZbToj96vipyyMOTP7DW08PFb3s/EaUeezs0a7/mhtLbUSmNvzpQIUG/7Zq/uBmxiyIIpL+jF1zbWgqrv4WbodxGPrG/QB0puCxZtr+q7pX6WCm9v8fUyRLtOMK/Y/I1XLIjxr+BsQtm58TKv00auTwCG9C/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8L8ovNq8zsjtv6robTL6xOu/vKzlXomx6b/gmVvropDnv8uyrlkabeW/n2ZizTFH47+2aEUwOiLhvyQkvYz4+92/hL7JXZez2b9rH5k0uWrVv8rYmKaBIdG/HYwViUOwyb9eWS/xjCfBv5DzRi1tWLG/WbAoZzu1Zb+5oHiCqG+uP4CzOnSf4qI/hu6QwXgnlL+TehL3yX2zv/Ae4Se9LsK/k5wHnH3Kyr+rnGp+uLXRv1AiEBT//9W/FKzdUa1D2r/kM3fpyoDevyQx/eoNXOG/hdF3ePRo47/iCEk4wGPlv8NeaXy2Vue/jsIhotJA6b+dNu4P/gzrv+byskf9tuy//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r/kzw+K4WPev+Meg14BX9u/Kqvk8L9c2L+4LtTBH17Vv6/Zi97QYdK/QI52YzPUzr/4XBa3IUXJv5tY892s4cO/QytR+zA3vb/cZjk+Ce2yvwO+j6H2k6O/OrC+OzEFg7+JgnAwERWQP1+hUot5WYE/uBz5Fc6wnD/dL9PBFzekPyySzBkTGKU/HbQizO3RpD+ZpXvZj/KlP6mY+VOwSaU/NL6an4OAnT9ujaljxZR7P45XhVV7cpa/GKgihNjrqr/QhQ548SC2v607U3F+8b6/h1ph5MaBxb+f8rsJMkDMvxH9JogpjtG/2qrYyVX+1L+/F4qwgXDYvwzh9wT449u/X1p1BCPP4r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T/+Jw3lKJrmP4t6eeaUHuU/+F8vi1Sm4z/Qe34+9y/iPwNHzNWmueA/YMyiCHuG3j9yqWFmgX3bP9SwBxyJX9g/PN4/rFo81T8xw4TRCRvSPx5Bkvmg5M0/q9GefWi0xz+a5ak20yvDP4Ol+l7WHsI//g3HfYRHwD+0ytn3gcKuP7cdDLaaSZY/JgetB6SvoL8+qSGoVLqov67CwAmB5be/nSXl/MDjxL8ey88ON2/Nv11vaN/2vNK/xI0Adfqf1r+/pd36dFnav6VIcv62Cd6/jB7HpmaW4L96OpTrzwTiv8n+qEP/XeO/D8MLBeeq5L+eorRhmfHlvz74VPEMNOe/p/SXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r8+lsIEO73dv5rF6+TYuNq/BJJCKBG117+edEDAlrLUv5p490Ksr9G/2KA1LMldzb8xn+TIhI7Hvzzzjkei/MG/KmyQxwbVuL/CPK91Grerv8WutFVteYq/VDZk+eTulz+y5CBCMr+bPwIGO4rv2Ig/yhTUvWgyoD96pQLaREikP5Aoflh12aQ/eYi/1t3LqD8Y23a441irP8M0jQMIu6Y/9xk+1tgdrD9WepL8UleuP0j8qwezSKo/3U877Ui3pz/qU91I51emP2ZNFkUqgKU/Et3TT1HHoz9kMs4vp3WhP76nKRAQdZ4/brPQK8dLmT88rgjEtMWVP+Qu86hPgpA/CoqeTDl5qrw=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T+C53m+H0nmP4sPyIVj1OQ/Ug2Zj99f4z8y1WPDbuvhP4SEGYm5deA/14hoVQL/3T/S7PECZvPaPzTftLpt1tc/g+15Km2x1D9bKlLWsYzRPyv4u0WV1sw/EOk5Bc+qxj8Hpts+DKnBP46gtwgG28A/HE9+UyQGwD81+12/bcuzP8xpcfwTZZE/yEhdxYYclL/Ys2tK8z+qv/ism7V6Cra/u62KdRohwr+75rcR+s7Jv6JsKDL2LtG/wdCytHUN1b/OQL+HOfnYvwFJ9/LL9Ny/iO4XIO914L9wHu4DlHXiv2KcYVF0e+S/Qkud4ueX5r8caO3MCp3ovzjKS9Inseq/AAAAAAAA8L8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6b+LjAxtgwLlvyk+sgzOU+O/DDZqUc6k4b9PNTrmCurfv2DiK4r/h9y/prKX8TMi2b+TILN9irXVvw5fd6iXO9K/kpmyL8Frzb+CdqRCzEXGv7ztSfkV8b2/URGpQFmprb+gBoZYKZlVP3bNzqmWZYs/CjBpa5IKlz/Lgh9V6xGePyxt4ctYSKE/JC7dhs+zpT9C8r7bMS+bP/PJRrkUG5Q/zFqLnCwXnj9Nyfgx92yvP/k/9Y+hSrg/Oi5rhUe/wD+MBaoQjZbFP/znaJ/Uq8o/Uou96gMJ0D9WgO+xPsLSP9Z9XY9Hf9U/r+ygu64+2D/ZEa9Gwv/aP737efIWwt0/XFp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "XVp1BCPP4r9zzQGUa6HcvwxEiyWVj9m/eOebnrV81r913Yzf02jTvwhVeThPVNC/TvrGNbR+yr/XIGywKVTEvxHKkuK9Try/lWR6bTgTsL8cgtkVaGWOv7EiHXpwEKE/zDt83TkttT/tqM5mRQTBP1zaXhv2+cA/G09S7MNzvj9w1TbATS6zP2TW6LkGJpg/UdCM0eKtnr/PJW9Bu1aUv7eurhso8YO/Qsz+T7y2or8p3RMJPdy3v0lls65WO8O/sM+utYR9yr8UgrJEhePQv5oD8z8jhdS/150XB+ER2L8frqqxo6HbvzbEjJJIM9+/jEgn2fBi4b/AGIiaWizjvwkK5tS49eS/qfSXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UukvN+/G079L8HAdV/fVv1kFMnXSJ9e/uoBRDZQK2L/vjiVbuTTYv1B69/l+Tti/iupQhABM2L9NDVKZr5fXv4ap3GNt59W/Xxl4QI8N1L9O/u36xvnRv0qkBk3X+c6/REt+UKspyb9KSKzaJiTDv53kdTVMsbm/32brbh+mp7+8YbdZFkqJP18JtiG8IKM/o9/cuKEWpD/LsqyoI5J2P0dQfxDbTJ+/D/43mpMzsb/8e02YKqy6v+jUX+oAsMK/P21ndIbSyL9NXbOS0HvPv7oxNBXRONO/ZikM4uRG179/6eG7zbXbv5CrY7ZIFuC//AhcV6JT4r9PnEJt8J/kv25E7CN48ua/qPSXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7r8U0tKpD67rv6clT6p3b+m/gnL3QJEq578/G+K3utfkv/MZu3aPg+K/qqxYN7kt4L+4E+6OEM7bvwieBxs/cNe/CcOMUUQd078cPUEki7PNv+J8Ph32nsW/xug4W6xIvL8Q2N8VCg2sv4ByYQ4cd1i/iOLZ01+Bpz8u32LRfP6zP78Ad8e1tpk/zm3u9/xrm7+4uEW/N2S0v/evQmzaz8G/mWknNAzYyb9Gogxq2e/QvxK95flOyNS/mo5c0iJy2L8dGku8ndjbv00pIKCoCN+/Yr4Aeeeh4L/rEgQMI2fhv8vE0kSvFuK/+5G8/bm54r/An0nux/ziv1tz25/tEuO/XVp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6b8tipRjsQblv36jVZF5GeO//IXZ7EEt4b/bQW2WmYTev3bJQScCrdq/1mdktQDY1r/CSWwuNxvTv+1IaGzj786/NeU5E3enx7/H0m6drmbAvxPsMhoQT7K/x1vPCVhFjr+AfYUKpdmIP3sYoGRJ9os/ihmEBGEhkz/iG9YRzGukP6zX5LJH2qE/aD9wRb+CqD8mwtNEjG6pPx9qclvkR6Y/cy2zCq6Jqj9stcgACvCtP7vDkZ3dD6s/mMgnYPGwpT+ulZHjd1yaP4rw2wvTvnk/pp414GXpmL9WN4H2YU2uvxGoeqKmPbi/eOqrNoy0wL+j3vl9pFLFv4AMXOOx9sm/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "X1p1BCPP4j/X1RvkYLLgP/qgI8T8kN8/OvfVuqC93T/zfduBS+vbP4GqasqXFNo/DzgiZMw82D9a0sjJfkDWP9z0Pgr/INQ/4od18gb/0T9CbXDULr7PPw2j77Wcdss/fggTMmzNxj9sWwTasIzCP09cE3fe0L8/yaKFyOByvD+f67QF9DSxP5quZ5GpfHI/ao5i0Futn789JRuFbHesvwFe3rA3qbW/YoTuo5wBwr93FaR8Ji7Kv3nYkr5dK9G/uNWFb+p11b9kwDDDbL/Zv6yDVdi4892/DMWHs2bw4L+ZoUslrM/iv0y1vjJ2puS/25+n3WZ35r/OcLWAukXov9oyJX5oEuq//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6b8uCzxUQ6rkv9G3/f5y4+K/TfCA7cQc4b9YeDGqs6vevw79joXkHNu/REuEEauM17+NbYsx4/rTv9bm65VabdC/gZI98ZkDyr8YRknFVyXDv2Y829Pjsri/Zrc/FyUeqL84F7iBs9NqP40s88kCtoM/CWgKiD//mD84AfwD2GmhPyVl+rM4mqU/9cKakndwpD/PkTBMZhSUP0h8Qg2+b44/sny5efTNjD/osjEUUQW0P2fcO77i48I/AhiuRxgRzD/M+8gRjJzSP30Y+/czLtc/pm9fBEvA2z/QWasjBingP+cvKBSiceI/Gha2p+G55D/wbpjjwgHnP/jyuhZMSek/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "XVp1BCPP4r/le9h9qWndv+CFsMtpg9q/rXbDj0+c1794mfzS6LPUv8Z3YcvBydG/Dr5wWZu6zb/neeV0utrHv2iGXYMJzsG/mjpcrSihtr++gPyrL5+iv7qMf79jLpM/ZLGO2FxAtD8JlEAa6vfBP8n66gxc98E//njnfWfSvz9y7TNDzdazPyif01+C25E/blh5uZ8Rnb+892IQHT2Xv62h9/eV34y/Bk51zVGTgz+nOj5ZcPWVP7g6n09ftJk/pZu+AdvKnT+/mcmMt6+gP//COSHmZJ8/SkERhfiSnD/R6SCrqq+ZPzYEG1qhxZY/sLcBWTfKkz/m3IpEpcGQP8m3a8acX4s/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G079GyrNGLb/Uvz6rN1Zrt9W/Noy7Zamv1r8vbT9156fXvydOw4QloNi/Hy9HlGOY2b8XEMujoZDavxDxTrPfiNu/CNLSwh2B3L8Bs1bSW3ndv/qT2uGZcd6/8nRe8ddp37/1KnEACzHgv3EbMwgqreC/7Qv1D0kp4b9p/LYXaKXhv+XseB+HIeK/Yt06J6ad4r/ezfwuxRnjv1q+vjbkleO/1q6APgMS5L9Sn0JGIo7kv86PBE5BCuW/SoDGVWCG5b/GcIhdfwLmv0NhSmWefua/v1EMbb365r87Qs503Hbnv7cykHz78ue/MyNShBpv6L+vExSMOevovysE1pNYZ+m/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j+mRRcq4BTuP0w26kCyuu0/8ia9V4Rg7T+XF5BuVgbtPz0IY4UorOw/4/g1nPpR7D+J6QizzPfrPy7a28menes/1Mqu4HBD6z96u4H3QunqPyCsVA4Vj+o/xpwnJec06j9sjfo7udrpPxJ+zVKLgOk/uG6gaV0m6T9dX3OAL8zoPwNQRpcBcug/qUAZrtMX6D9PMezEpb3nP/Qhv9t3Y+c/mhKS8kkJ5z9AA2UJHK/mP+bzNyDuVOY/i+QKN8D65T8x1d1NkqDlP9fFsGRkRuU/fbaDezbs5D8ip1aSCJLkP8iXKanaN+Q/boj8v6zd4z8Uec/WfoPjP7lpou1QKeM/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D9gEiaLihPpPyyUvF67tuY/zZzp0d1e5D9/r0lEngviP9bsmVPudd8/HxJz9EsA2z9Q0SBWEbfWP1DZiD2AntI/eXMCtGZqzT/vSG8TjFXFP3e9a/K0Brg/Sm3E+jKAmz+AOpcLpo5vvwgv7ufVD0M/4FLPCSjUbz+yl4fixf6lPyYvca0Jsak/Vh3n26SGnj/PRJe+li+bPzAZXvCHuo0/gArIFtCZkr+TOXdnE/K0v8QU/u5FKMO/zsh3BSKzzL8bzMQmOR3Tv6TfGr+inNe/ExvMksLz27/BIJto5Qngv6RDUowrKeK/lsSQFpVO5L9Ignh1PnPmv56ZJjXPmui/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAACpFZnFoiVrP4bq169sboA/i7Dsf0+Gij/wuT1mv9SRP4c3Lo2y95Y/M7PmTyvamz+gr1n6O46gP74/aggR/ac/OOxwpKr4sD+UMsixN6K1P7mg2H0947s/N9a2oDJswT8IshpYrBLDP0L2+phKssM//lD3PWLIvj+YJ95efvq0P2AlZZ+LDF8/IB2wkZExgL/AStaDBnmAv6z94LW3Zo+/deYEQhiQiL9N2ebm0WWLvz0U9iR46YW/YRF56Jb8h7/NBtKDV46fv97YnSpbxKi/lMBWh1tqs78h+aVIRR66v+e6Gu0P3sC/CTiAcJKcxL/ceHCJOEvIv6VQTZz1/cu/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+IbasUC6bhv5gl7VMTtOC/dk6tjI+I37826RZ0akvdv+MB/za5B9u/OyerlwjI2L+u3O40LofWvy+XrT7mPdS/jTc4NxLf0b9kDjufx4LNv55Xh7LnRce/XoHPntFZwL/mNhUcPj2yv3U8o9CoHn+/WDznTGqGmT/gdlFuMXyiP2+x1Y9m0aM/+pl5YzMWf797ZaGEQxuzv1u9XCu0EsK/q/GqlTWjyr8cBOk88JDRv70eP+r1xdW/EoZzBgPv2b9OsAWSAhnevwRPMaWkH+G/DcFQ2+sw47/PQR5mcEDlv9gzbQzRTue/wcm+Zotc6b8K2sizDWjrv9z5zyQ1c+2/AAAAAAAA8L8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T9Wn8rdalznP08QiVtNkOU/UvvCFlPA4z8rVugxHQTiP8u8Gk82T+A/8tRXt/ow3T8bU+QKEMPZP9qd9xBxWNY/Bn6fLcP+0j+LrlKB5T3QPyR7UkKgH8s/sdJZU1i7xj9tmhu7QuvCP829UZ7taME/IoD1cHmPvT9XP0TZqVuwPwteqJAuB4E/5YNL09WflL/65hx3qEOKvwjytllBano/52GEDzbQlj8XEjRS4xmiP+gBqbcTw6Y/isO+eIolqz8mS1ihiBeqP7mDYMWpdqg/pvZzxbs6pj8cK94THOGjP0N43Hd8S6E/iJ+0dWpRnT9rRg+tOTeVP/0M08PgXIk/B1wUMyamoTw=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6T8rBNaTWGfpP68TFIw56+g/MyNShBpv6D+2MpB8+/LnPzpCznTcduc/vlEMbb365j9CYUplnn7mP8ZwiF1/AuY/SoDGVWCG5T/NjwROQQrlP1GfQkYijuQ/1a6APgMS5D9Zvr425JXjP9zN/C7FGeM/YN06J6ad4j/k7HgfhyHiP2j8thdopeE/7Av1D0kp4T9wGzMIKq3gP/QqcQALMeA/8HRe8ddp3z/2k9rhmXHeP/2yVtJbed0/BdLSwh2B3D8N8U6z34jbPxQQy6OhkNo/HC9HlGOY2T8kTsOEJaDYPyxtP3Xnp9c/M4y7Zamv1j87qzdWa7fVP0LKs0Ytv9Q/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "YFp1BCPP4r+6aaLtUCnjvxR5z9Z+g+O/boj8v6zd47/Jlymp2jfkvyOnVpIIkuS/fbaDezbs5L/XxbBkZEblvzLV3U2SoOW/jOQKN8D65b/m8zcg7lTmv0ADZQkcr+a/mhKS8kkJ57/0Ib/bd2Pnv08x7MSlvee/qUAZrtMX6L8DUEaXAXLov11fc4AvzOi/uG6gaV0m6b8Sfs1Si4Dpv2yN+ju52um/xpwnJec06r8grFQOFY/qv3q7gfdC6eq/1Mqu4HBD678u2tvJnp3rv4npCLPM9+u/4/g1nPpR7L89CGOFKKzsv5cXkG5WBu2/8ia9V4Rg7b9MNupAsrrtv6ZFFyrgFO6/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+nL7GVVrvevzwr3UWiJdy/aB6+96CO2b/vfdhrx/PWvzhyKKqkUtS/BrMhOk6g0b/wAfBQ0ZHNv6xhd9gNVMe/cRqLIgIWwb+JgGQtWAu2vy6+nX5nmaS/sNm5qORcbz8cy+mxwWWKP51DIBJJWYM/7GJMIHlikD9w4zkI+X6jP4L9Vvk8lqU//wxpH+xlpz/F1IR1qQCgP7XvdEdAk5s/I6NRZoQUsz+quSLjHzLAP80BEVOkxsY/Kn0CHxJDzT8439M6mtjRPwzYzn9wL9U/YHkLNW2Y2D/87cxgtwDcP/gTJaNbat8/rIYrz3hq4T+ksIPoRB/jP0kFsh+O1OQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T8MVjmdAUblPyzeunzrquM/0MBOYsUP4j+wGaLzonPgP/K/X6gRr90/w4piKYt62j8MyI+AYGTXPybq+O+3k9Q/GPAEUEHd0T+37RWzZ1fOP9P7h0dr68g/oykZfCLpwz+uvXiy7zrBP77DiLYbZME/VTso5IoXwD9EGHShA86yPyaDUd+L85g/HiLA5cN0lr9294NoZ8uXv8yfZWW10Gy/WnhTU1i+pz8gO6xa4GK3P0IbLIIOT8E/vz7o2lndxj9MTFYOiljMP83kbRnDttA/iRgpNrcc0z/PdDi8D3/VP1BSCeyq4Nc/Zl0h7yNC2j/fX1l4aaLcP7QPeL77At8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6T+mn28/TQ/oP0dDfONNueY/lvyWCgFm5T+ss+fT8ALkPwN2s5JXneI/bpwx2cw24T+249mPQKLfP9PhQ4zYvtw/hGUjDPX32T/NENliagrWP4t6lRT34dE/ecoe2zl0yz++swbaHRvDPw+ux8s7BLY/iITPCvfyoT/funF2dpegP00fxrxp0ac/LVRWhcT9rT8UAqoxNae/P/I31hJbicg/DL3bnU6G0D+wiDbyUKXUP+nC+LROodg/uIRfnIqM3D/hBEwalRzgPyfmDENa8OE/pKlDy6XV4z8ByGJ3OMHlP6f3i8Ftr+c/TbxDrsae6T+PlzbG/o/rP2RirXjPgu0/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "Xlp1BCPP4j9rnbu35srgP074LaySwd4/GtOVDQns2z9XmourKjHZP0OTUhuWhdY/LfewWGzg0z/8N7TWg0LRP3u9GM31y80/M1gOxX4+yj9vKlPB/7nJP80MKSrGssg/kLaSV9BVxz8pbJGPPbfEP+O+ZFEMrsI/ke1SDj/9vj+y2fscBMaxP4ht4SfFYYY/exLnI0C8ob/C4EEpNRiqv4QSfK7DHK2/nqHdsvnTq7/erCBdJHGpv+KCajXh2aO/IqCsa1WDlr8SC0BMMbZ5vzfmAHv0Mmk/k8yauP3Lcz+8sKkuCI5zP+HN4TIGk3I/14SGgrNgcT8MSAIJmiNpP7ENYSFWXF0/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z+vr+3HwVLQPz8reIfDSc0/HKo6CpPtyT+9TlOPApHGP5NeoeBENMM/RMyUa3fmvz9q7Qvianq5P+ta0X7bLLM/TvxSOQfjqT9d2mAJdwucPzC2MS/19Hc/+DISBoj1db9adImgsSxwvwAlHHPGq/i+4qoeefyvmD9WNZkqN+GdP8CrYxn8SaE/xEhcwwT5lj8xNINRbemiv7RkXKMfQ7m/9iVi/ezDxL/e1LpvhBbNv2QzNk62vNK/xG8izFLz1r95vchTNSbbv6fcxawRUd+/w6vxlMO54b+r8+mT37Xjv/+eylnjkeW/jzLey71m57/kJK9JGDfpv7t8QlRNBOu//1REEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j86Crno+p/qP/H5tf6Hw+g/DEY7a53m5j/8lH+6WgnlPyjn6xehK+M/1pJ1bsVK4T+sg7/sfNDeP00FCUv8Dds/CVRdzdZJ1z9fZU37FYDTPxg21/2hYs8/HhpXOlCxxz9Uxg2JGQnCP6/Sh1xkdsE/CPwIX97OvD9J9SeMMJKxP77MBcW/hIU/L81W1/I5l78iTGHkwml3v8DT6pWNPpE/gurXalN2oD/s5kx7bDmlP5TUaqI6TKg/tiU9vFfaqz8ql1XKJwewP2imfvo167I/Lf24EaJ4tj8Oj11kfcW7P5jOcG2swME/AdkFzbqvxT/lIkEvAqjJPzt1ubFdpc0/UekvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G079GyrNGLb/Uvz6rN1Zrt9W/Noy7Zamv1r8vbT9156fXvydOw4QloNi/Hy9HlGOY2b8XEMujoZDavxDxTrPfiNu/CNLSwh2B3L8Bs1bSW3ndv/qT2uGZcd6/8nRe8ddp37/1KnEACzHgv3EbMwgqreC/7Qv1D0kp4b9p/LYXaKXhv+XseB+HIeK/Yt06J6ad4r/ezfwuxRnjv1q+vjbkleO/1q6APgMS5L9Sn0JGIo7kv86PBE5BCuW/SoDGVWCG5b/GcIhdfwLmv0NhSmWefua/v1EMbb365r87Qs503Hbnv7cykHz78ue/MyNShBpv6L+vExSMOevovysE1pNYZ+m/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j+mRRcq4BTuP0w26kCyuu0/8ia9V4Rg7T+XF5BuVgbtPz0IY4UorOw/4/g1nPpR7D+J6QizzPfrPy7a28menes/1Mqu4HBD6z96u4H3QunqPyCsVA4Vj+o/xpwnJec06j9sjfo7udrpPxJ+zVKLgOk/uG6gaV0m6T9dX3OAL8zoPwNQRpcBcug/qUAZrtMX6D9PMezEpb3nP/Qhv9t3Y+c/mhKS8kkJ5z9AA2UJHK/mP+bzNyDuVOY/i+QKN8D65T8x1d1NkqDlP9fFsGRkRuU/fbaDezbs5D8ip1aSCJLkP8iXKanaN+Q/boj8v6zd4z8Uec/WfoPjP7lpou1QKeM/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G079GyrNGLb/Uvz6rN1Zrt9W/Noy7Zamv1r8vbT9156fXvydOw4QloNi/Hy9HlGOY2b8XEMujoZDavxDxTrPfiNu/CNLSwh2B3L8Bs1bSW3ndv/qT2uGZcd6/8nRe8ddp37/1KnEACzHgv3EbMwgqreC/7Qv1D0kp4b9p/LYXaKXhv+XseB+HIeK/Yt06J6ad4r/ezfwuxRnjv1q+vjbkleO/1q6APgMS5L9Sn0JGIo7kv86PBE5BCuW/SoDGVWCG5b/GcIhdfwLmv0NhSmWefua/v1EMbb365r87Qs503Hbnv7cykHz78ue/MyNShBpv6L+vExSMOevovysE1pNYZ+m/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j+mRRcq4BTuP0w26kCyuu0/8ia9V4Rg7T+XF5BuVgbtPz0IY4UorOw/4/g1nPpR7D+J6QizzPfrPy7a28menes/1Mqu4HBD6z96u4H3QunqPyCsVA4Vj+o/xpwnJec06j9sjfo7udrpPxJ+zVKLgOk/uG6gaV0m6T9dX3OAL8zoPwNQRpcBcug/qUAZrtMX6D9PMezEpb3nP/Qhv9t3Y+c/mhKS8kkJ5z9AA2UJHK/mP+bzNyDuVOY/i+QKN8D65T8x1d1NkqDlP9fFsGRkRuU/fbaDezbs5D8ip1aSCJLkP8iXKanaN+Q/boj8v6zd4z8Uec/WfoPjP7lpou1QKeM/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r8Y8jZlMRLhv7O0LD7P8N+/JELzIL243b9gvtKI8H3bv33QP9kMAdm/gXhOxD+E1r8fgLMhk//TvymSYXd1etG/YOM8LEXJzb/eHYHBR5rIv6elp2fdtsK/eKRblnheub9yHbhl3L2lv6NtcwNAemk/G5+PfP9smT8hT9kyqgShPyr4z21DTqM/m3j/xjzjjD9OYC16bGepv9bFFac2RLy/zk3JlpOLxr/AsJatmwDPv1fvzGdQr9O/KWAwHkip17+sDYdcbLzbvyQxozoWxN+/VbZL3kji4b9beyXnBNXjv3PhdRffu+W/AGf31DeR57+gUXqJlGbpvxQ8gI+OPOu/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T/QBBi6gezmP6gWPy4vKeU/fcezGFRm4z+HSr6Uf6rhPye2rgl3AOA/ZyQ4aGyu3D/lskQDX2HZPzh8f5ggF9Y/AF/NYH7e0j/bz4v11IzPPwAuZLxVD8o/DKXuKmZCxT+uavuv2ArCPwy2WYOEO8E/UfxdpOmtvj/4ra+H7HWvP00fOSzWFoo/6MmKNBaCmL/Cdnjq602Zv4EhqGjUI46/jpLYIeT2g7/sU8IbMLeQvwUKptedgJy/zWercXoIpr/l4QNVRdiuv7fbbTruW7S/KjKL8fV6ub/64bOxn7y/vyUBNiFvXMO/lA76UExTx7//Nll9o0vLv4fGS56ZRs+/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j8MX3b2OzbsP3oMG7WdQuo/KhRq+DhT6D/X/mjMVGfmP1TJQLlofOQ/DpIR5QaP4j99VUC4FKrgP8ptnyB2k90/2nna0E392T8cycqR7SjWP0KWHD0wStI/pvmJVV+bzD9acyD/VqPEP0fS8dCiP7k/PCxz7Te/pj/298XCbruhP223bCRKhqc/1cEZRmnSsj+54Z2E6RnBP0w+rXrgSsk/uSVbbynD0D+alENxAuXUP9l/R4Fr/tg/AkSTt4zK3D8CBocTnFfgP8rd145HSOI/A1MTObg65D8aZI+HEy3mPwv1pUITHug/a5IEfiIO6j97y6pbQfvrP/RF65rw4u0/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z8Ah2fCY+rTP9WqfTpOvdM/JSUFJNlM0z+dWMm2fqzSP2QPPEtn/tE/wc9EQ45E0T8WBeefQD7QPytZvxwz680/ROdwPsQcyz8txJ4vOPrJPwoc1AoTuMg/IkPC6Urexj+iCeoQUcrEP+jvDKpSFcM/dz1Pep0+vz9CzTqA0BOyP5QDdA/g1YU/dfJI4li3n7+Qysk/Vtqov6LHvgoTWq6/ykn3EN14sb9q8IY1uPyyv+AVIapV+7G/4rmmnSxEsL+Hk8XHHaOvv8p7G7h+J7C/JQ78fESyr79+ymbOTd2sv4hclgKp4Km/AxxXMeCmpr8w9mcWn+Chvzla6c5Zm5W/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "CoqeTDl5qrxuIR4FTD2bv3K/JVKrKaC/jgT1mqueor+H9cWpZvekvzRJVqfjJae/9yo70podqb84y38hF8OqvzM0lyJzG6y/i1Wd2i03qr9Yim/WUEKmv9wWV4+kL6G/4pMS/vGzlL/CS4SHNPtmP6P9JHawIng/SE2hfNOEoj+10AxBT+CePwzqbHuEU6o/Mp9GGllsoT9DBhAYbZiiPwzIHCsy2Zc/YxYODgU8kD/6Abmt4j+jP/6L9Khkmq8/clX+UVpJtT/touEiUWu6PyQBG4Azg78/XVi3pKxIwj/+14OUHNnEP0ZZWGCKcMc/x/R19wANyj+I74i5G6zMP6Ph4vINTc8/UOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8L9218TeR1/ovwauk+UF7uW/u/9WaW1847/B3ybXcQrhv2uizTgTMN2/2PYb22BK2L+Ez/+vWWXTv0BpGGGo/sy/TkV4vfE0w78UgmcyYeGyv2imPLj2FWE/55DvneSxsz/vxmJZXSnCP5J4LsRdX78/riKWZR76uj8WaXt0suKzP2jBCPl+X2M/Lm3MRWtwpb/GaiLLH1Omv2g7bU9TUbK/CmQKSPEIor9M8pQOh5OYPy4FEOoxM7Y/sqGH2Id5wz+N7AF+bT/MP1JJQztxndI/sg5DDD4n1z+E84rWjb/bP85ndkbIL+A/XwsN+MSB4j8tROLUWtXkP+LV89P7Kec//1REEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6T9zh/M3CXjlP0xnGvDcvuM/eDUpnLwE4j9k9IS1a0ngPwmmjBEmGd0/fl8mgJCb2T9Lxr44JxjWP8ajWDxLNdI/ghDVIjB4zD8cLKQ4IrTEP4/0J1ou1bk/6WJXx+G+oj+qJVR1H7BjP+ZEP2HaKpE/g06FsqIelj/3a5wQvpyjPxrzT4CY1KM/rTHwHy8Vpz8DeaSQCNuTP16Eg0Ir+Is/o5lzVHbol78rWOxRp0q3v6ll9tI/88S/tDSasxxazr88lRI5sPPTv4LEb7FaRdi/r1SFl2yK3L/gx7xMi3PgvzIcTgSrf+K/yGmPucSW5L+ejzUvW6zmv9373r6Kz+i/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "Xlp1BCPP4j9Wb1tEu73cP6gkhYOe6dk/5Jq1GBwY1z/W/xqmGkrUP1t5Dx3wgNE/OGekNJ5+zT85J4MP0Q/IP1wRIP4aUcQ/ODOqy7YxwT8buaCgNbi8P6f2lV9nFL4/ttbYvRp+wj9wfWGSHUzAP3jd5sL6rME/xKfzb+rKwD8/Cl/7OX+0P2I6my/7qZc/NDNyLM+Rk7/dctzY8pagv4ZBoT+QwoG/xuenbHx6gL8qwyifJCKDv3olX/Yxq4y/2CeczP72lr/2PsNhdkSgv/kCMjEwlqu/W/AFQy08tb/yEttnxu27v6qLJTJMV8G/3U4F+ugCxb8LcAHz36bIvza7ofsNS8y/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+nL7GVVrvevzwr3UWiJdy/aB6+96CO2b/vfdhrx/PWvzhyKKqkUtS/BrMhOk6g0b/wAfBQ0ZHNv6xhd9gNVMe/cRqLIgIWwb+JgGQtWAu2vy6+nX5nmaS/sNm5qORcbz8cy+mxwWWKP51DIBJJWYM/7GJMIHlikD9w4zkI+X6jP4L9Vvk8lqU//wxpH+xlpz/F1IR1qQCgP7XvdEdAk5s/I6NRZoQUsz+quSLjHzLAP80BEVOkxsY/Kn0CHxJDzT8439M6mtjRPwzYzn9wL9U/YHkLNW2Y2D/87cxgtwDcP/gTJaNbat8/rIYrz3hq4T+ksIPoRB/jP0kFsh+O1OQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T8MVjmdAUblPyzeunzrquM/0MBOYsUP4j+wGaLzonPgP/K/X6gRr90/w4piKYt62j8MyI+AYGTXPybq+O+3k9Q/GPAEUEHd0T+37RWzZ1fOP9P7h0dr68g/oykZfCLpwz+uvXiy7zrBP77DiLYbZME/VTso5IoXwD9EGHShA86yPyaDUd+L85g/HiLA5cN0lr9294NoZ8uXv8yfZWW10Gy/WnhTU1i+pz8gO6xa4GK3P0IbLIIOT8E/vz7o2lndxj9MTFYOiljMP83kbRnDttA/iRgpNrcc0z/PdDi8D3/VP1BSCeyq4Nc/Zl0h7yNC2j/fX1l4aaLcP7QPeL77At8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D+SwjxldmztP+aFaG3mces/9s7lozJ56T8nJhFT83vnPy40HPbYgOU//Ht2jNCA4z9HhVSHUnfhPxhnYTuqvd4/LqmOUvhF2j9y17Gk4+3VPwRAVmjlytE/ciuAAnKRyz9ybKerExPDPzPp3vTnk7U/iXzC2BKroj8jWFdy8TOhP6HG4Vf6fac/XKWr/0Pusj+xntTlEx/CP1H6N7Ka+Mo/fgje0PoE0j9BqCSn8WXWPzNwZgMmbNo/28vJr+kG3j9LI31gJ4zgP/BA6nQ8/uE/FNcWPZhZ4z9SSxb6O6rkPz14YX8a7uU/AMZ8QsMI5z+Not0rgvPnP9uhEhzJ2ug/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAADDdJUspKGhP0iEEqg3Mq4/D7ka3RZhtT/hC8QthPO6P4dVPVslSMA/bcFTQWT7wj8EMMJb3a3FP5rUgUE/JMg/HmCNWnLryT+9BByi877JP5ApGm7158g/IWSdPVnOxj+SiRBNo9/EP6UxxsHi88I/yYs089xJvz/c9h+dAIexP4H5S7ZkG4M/XE+bqo56ob+SAO68jViov1+RdZb4xam/aZXQUVZlpb8QjcBfkCOXvwHtiyk0EIY/k8DmYxssqj/+Fce5XQG6PyiCoYH2qcM/vF1f6FqNyj9EZmPeTb3QP28/u5zBPNQ/qVWYaBrk1z8Wel1FOcrbPypPtrx0sN8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z9tVo6H7o/QP5sOQyAJfc4/RfRsIXnPyz+4znvt/BvJP0OPV5NzZMY/3RJwEkOqwz8emOHctOfAP/ouxDZUa7w/tEM+vdxeuD/JLpugGNmzP8tSQGoWH6w/uHFK7IqpoD+Uo8bKektnP4CpMDVZIy0/CDxVKAIDkz8GrvV6FfCiP5yi8hUNyqA/ykkoize3nj9azh3lgHyhPxiHEMK7ZZI/D9TONFZgfL9LKxMvr2Ogv0PsxPJTOa2/cXoKQdMftb8+376LSnO7v8H46UQl1cC/+xc2sXbxw781t8HWIJ7Gv3BJ0T/VOcm//V3Bj8fWy7/RXNmJfXTOv3oRZAV/idC/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j8BHAGg4rbpP1UGlbct1Oc/TI0WDY7w5T/xYijARw3kPzv/GWvpK+I/jQzNRD1O4D+XU98gquncPz6u2NY7Rtk/ucGUI9OY1T+VCL4Bz+LRP7e5CuL+ucw/S3wXtX8pxj/81pQoqyvDP6RJNyowv8E/aA2C6Q2Zuz8CTNz+6QSvP3A/01Z1310/IaXlTsGJmb/5YCbYA2Kov1LPwIXxzbS/hPY+a7Djwb94yP4E8p/Jv5pLUh2atNC/fUQIX5+V1L+I6J0agXjYv+geHoGlTty/QC3ail/737+EZPoLMN7hv4tY/JUryuO/9JL3wa+55b/LIL20e6rnv0i2zc+9m+m//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "X1p1BCPP4r9GQfMo58/ev2jM3OSwb9y/VHxawzIL2r9qS7BbDKXXvxpssGBbPdW/S8OkyEPT0r/itXnxOmbQv9MjexER4su/YGctaPbHxr+mZMf0QJ3Bv4RHJLa4gbi/fnjhqIW8qr+Acktch5tkv1j3rEdxW3G/QMRKl0BWoD+Kv3uI0S2mPzIHKFTuSZc/Hn1LTBx8pD9Ud8XKSDGbP2fA6icb44A/ll+uVermfb+XTg+TNZuaP2K6JJ/8GbI/sIbRcBnVvj//rn6OrQPGP9z/7kLM18w/uIHU4lQN0j+YYRU4DvbVPymJ9GjiEdo/cuk7UQNB3j9PeW3FEDrhPyEPH7hUVuM/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "p/SXm3fj6b+30DGwTsLjvyoiCs+6quG/rT6XjlAq378nAW2a3QHbvzLe6bS83Na/Jjm2M6S80r+AJnZvqUjNv8N2EICpGsW/TOjIwpMDur8WCEn7/eujv0naDS6heJY/uEfyrTEetT+Tq5UMS7rBP6DZBTLbor8/t7gIbQlCwD+2BIAMqPeuP/xHFEd+h4M/DPKGeF2vm79cFYUExOSfv4Clh62evZC/xvCxqPoihD/mNffLkvWxP9Q07/mLgcA/MAelQp7Txz/yK+IH+dTOPx70Dyz5j9I/fFe6rH8H1T8a6PW230HXP2OZk0I7dNk/NzvVYBen2z9sKlzr19bdP1veOqFhAuA/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D/UcX8dKtDrP96kC+7Isuk/Rx+FDySX5z9PM9ozP3/lP2lVXqAJbuM/TQVuQ4JZ4T/X4JzgPmTeP4A6t+b8C9o/aepmCT+o1T912YmOhzDRP+uojrj+Z8k/i17ITsA8wD/78qLYfIWrP/U84XkdWIg/b0Lje4SOmT8/NzTmMtKhP8dNEvIjLKQ/7wbBJjMOpD/aqT4LZAOxPzx5DVOzELo/KIFqX2fHvz/Cyh/N3W7CPxJGXOyNuMQ/U9dh1k7uxj85HZhpNw3JPxjDa6BmKMs/JYQIfI85zT/Ct77QtDDPP6DDn3ejV9A/uC9NhYwV0T8wZDb2b9LRP3rnX4OJjtI/UOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAACGmaLgSo+dPyHmB2uSx6U/AkL7djXLrD9ZqabmFvSxP5DabCMrv7U/TxYEfWnUuT9QBYrpc5S9PxRGP2ZkzsA/sNJAZdOzwj8u50DoZzPEP6gucOP5TsU/Q6oO+oZfxT+Dq6RXX4zDP/okBQgSssA/0FIqvDLgvD/U78fa70GyP965zOUR+H0/pgA06T8znL+JnHBcfh2gv/JPyDv385I/PmzJoHpctT+9iomd5SDDP/8aWAxaocs/4q3ULggR0j/VWN95QFLWP9PeWl1ek9o/9Fd62uXT3j94rG7XgYrhP8UquusgseM/3A5DvlrX5T8JZGQ6Qf3nP3L7jGPdIuo//1REEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "X1p1BCPP4r+wreEALO7ev0GJake1jty/s4uox10x2r87iipjDNPXv36CcmgvctW/FzhsIl8O07/7cj4j66XQv8X/3bWmUsy/0A0yTIgVx7/UAzRhH8DBv7YL/7aYj7i/rMUPRLSpqr/Ez+6SwY1qv1xx9N9qS5U/wS/JmvA8nz8qRAh8ZQiaP0RxpnoDYqc/YUsAXXkOoj8+Xs8MlMKhP+TcQbRq95c/NCTSFX/lcL+OBUck4Taev+lrv2cttqy/kzTFrAYXtb86LMhhgbe7v/Y64rMa38C/5yfL/Dydw7+Tnr6Gc0fGv0lTfEaLysi/3ysjFPx4y7+R6bTAjQbOvzGunlI7VtC/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "p/SXm3fj6b9fQQ1ZFrjjv8EOSBXRn+G/zPAnJ/oN37/9XqzEed3av0RXU0qBr9a/Z2OcKdWE0r9bxFHtIcHMv9yPJiJulMS/E+BUr1cuub92jFqMNuqivx57sp15GJc/8Bp5KA5jtT9M/JrkFafBPzsHS9xsk8E/0KTzwKvuuj9CX22eAVayP2g6dt3FC2I/bi8TPn0so7/BQwmdnl2mv8QHHosFnrK/2yiIpMsbnr8kzCzXm8KdP501QLezErg/pSrI+NPAxD/DdtPCP6rNPy+O3hDfW9M/1Wep0P/u1z+42dE1qIfcP917EUdQpuA/BdCS3Hbz4j8SL7zQQ0rlP95F75lnrec/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6b8tipRjsQblv36jVZF5GeO//IXZ7EEt4b/bQW2WmYTev3bJQScCrdq/1mdktQDY1r/CSWwuNxvTv+1IaGzj786/NeU5E3enx7/H0m6drmbAvxPsMhoQT7K/x1vPCVhFjr+AfYUKpdmIP3sYoGRJ9os/ihmEBGEhkz/iG9YRzGukP6zX5LJH2qE/aD9wRb+CqD8mwtNEjG6pPx9qclvkR6Y/cy2zCq6Jqj9stcgACvCtP7vDkZ3dD6s/mMgnYPGwpT+ulZHjd1yaP4rw2wvTvnk/pp414GXpmL9WN4H2YU2uvxGoeqKmPbi/eOqrNoy0wL+j3vl9pFLFv4AMXOOx9sm/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "X1p1BCPP4j/X1RvkYLLgP/qgI8T8kN8/OvfVuqC93T/zfduBS+vbP4GqasqXFNo/DzgiZMw82D9a0sjJfkDWP9z0Pgr/INQ/4od18gb/0T9CbXDULr7PPw2j77Wcdss/fggTMmzNxj9sWwTasIzCP09cE3fe0L8/yaKFyOByvD+f67QF9DSxP5quZ5GpfHI/ao5i0Futn789JRuFbHesvwFe3rA3qbW/YoTuo5wBwr93FaR8Ji7Kv3nYkr5dK9G/uNWFb+p11b9kwDDDbL/Zv6yDVdi4892/DMWHs2bw4L+ZoUslrM/iv0y1vjJ2puS/25+n3WZ35r/OcLWAukXov9oyJX5oEuq//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UukvN+/G07+f0Mms54bRvzI6Fu+EedC//fCaDfDOzr8BU3pzxKLMv2P1rOAybsq/aPd3xQQsyL+yzgLoz3PFvzoshiw7bMK/5nvZ8NuQvr8bBsixuQO4v+Lm3mwo7bC/knGfjBQ+or9svXhx2Otpv/bsynxXv4A/SjafeuFfoD8+09YG322bP9DB63v00aY/GUSgeoIzpT96FEwa7e2mPzJyJ10NE6Q/hv9HuvUfnL+MLyZgIOS4v+zGYYPdbMW/NnVv6T9vzr8e+wf1xLvTv4sDIEH9QNi/G463zwWs3L+BYUZoUYHgv9YWfCuKq+K/Rbx2ZbPU5L+/xVjlAv3mv03btKGKJOm//1REEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7r/xfRhwMa7ov1muCCw7WOa/zx1t18MC5L99OY9Nmq3hv4avIAYfsd6/us3sgFUJ2r9S0sJWKHjVvx2iM3Ae6tC/X8Bxqs26yL/OqSP4YkK/v4453T7QVKq/SJKZhLxskz++yCa41cW2P6iggowd2MA/zIbiZjGkvD8k97XUgm+yP+hQhIsVc3I/nNVwhvICo7/ZMpwqqtOnv1VQEuxV3rO/Pz2mgUNks7+QJEPBQ/2tvwvQqBLB+aK/b2JpFqF/ir9LJwcM+0qIP86/o12Z76I/r82LBW7RsD8zhkcdV+e4P1gtsxBlgMA/46+9dEmOxD8QmCXNG53IP8b1CkdlrMw/UekvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6b8jnGQEIOXkvzpbHAo6KuO/KfjK4Ntt4b9qt/8+pmDfv1aDIdYP49u/7PbaaGRi2L8NzEKbBt3UvxU+CO2dTNG/FU2JU9xay78byBs29+TDv6vbdooF2ri/mxzVpKifo7+sBrcZ9wuEPwOyte3qYHU/7T6bLywGlT//qIlnVr6WP1dAM+3wEKM/srBrz2G+qj+Sx8IcK82dPyIDJy2kQZY/jozFnZ3dkT/ndiEPa46wP/hSgjExOb4/edNMxFxKxj9FLEj7aW3NP3TGoFzpRdI/1ddr2fPi1T+MeE4jtonZP4Ab9mVpN90/DDXCBPl04D+gaYXwEFDiPwhC4HeLLOQ/p/SXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "XVp1BCPP4r/kVHAyR+Dbv/Wt35y9h9i/06W03H8w1b/9+vRl/trRvzT3oytzD82/6UGw/3Rvxr8UonjZmrO/vwMulWMYh7K/m91szoeElr+M2P2iFYqaP4AUf4Cl2LE/OpSI8/41vD/KOCD1XWDCP+m2ujVgxMI/AN/iJg6Yuj+dz6FxcLGzP/QcF4k4oXA/Aao2VHSpmL8KRJfItnyiv6BHhw9wwl+/iaFU1pNKb7+EKt4E3DGWvxy9z1/ycqq/HVW3DdCytb+jq0wR1S+/v3DQF3VBHsW/F1rtXOjgyr9Vd39QJlzQv0UXuW8KTtO/x4BsGQdE1r/CHqv/ED3Zv3ENIv5fONy/YFp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G07//2zvqyG/Nv6Ka/DMT/sm/wR6IdhCJxr/blYoFLxTDvwBlynLzQ7+/kF+ARfppuL97YT9z8qCxvwsfmmP5CKa/tGH1xXE4kr+Yc7FiT/h6P4O/2MC2mJ0/987/qSZRoj8i8Sz2y76aPygf7kuCmm4/dBIwwfldnj95pCaizgCmP4w2TWDu8aE/FIAOsSBkpD8ihXgk7IiqP2ByO5V0GKc/czeUaocnrD/spKF3GuyuP2ywS/7iPrM/7Phg/CTatz8yalM6oa68P4iLVy+r08A/UWVqd/Jowz9Or5Yah/3FP2wQ/wrIlsg/PGOSvXsyyz958+UK1M/NP9Xt8lqjN9A/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j8bczYwGRXpP3YJC2ZCOuc/b97FzOpi5T/PX8kYoY7jP9EIbg88veE/5ucyycDe3z/1CvaqOEbcP/9PTpeVstg/YWSXsb8i1T95h7EpHKXRP+zxpqZfiMw/JTERE420xj8HKh5xzq3CPxJosdoZYME/7Q0fRkOgwD+YYoLiXia0P2LGHgKyyY8//YQyW4D2kb/uZyvty1Kmv+NNRT+TwLW/G/vz70/ewb9YUH0rS47JvzOjupq9bNC/mUCHfAYq1L+FjVN3le7Xv84YuMJet9u/H9xRNUZ4379JzPCZFJ7hv2s614x7g+O/USv/sQNr5b9m/qlRIFTnv3j/jJxxPum/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j/1cHo/lxfrP30WQwl2hOk/jHnSynHo5z/aXmtrgkDmP5TAHr8hiOQ/BmnFAoXE4j/F4Q8Fgq/gP7Zq46xD+Nw/diay98qA2D9rUK090BDUP2xIUzj6yc4/8r6DTUrNxT9sRYlriGa6PxTdZuplW6M/2d9/DX9YoD9oqunexYSgPxjpk7T8bKk/WFS1ozafqD9U/XlWwIu1P6KDGbyprMM/kgp4ODg9zD9oVQucV9XRPzs3CqeJidU/u3w44vKb2D9XzTnHwGfbPyQnQpwPA94/XJ3Hdm8S4D8+9M3s8ZzgP3n0W94pG+E/jKO2VBmR4T93oMQVoQHiP9dCqnNCbuI/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G07+wppvd28zDv3wxoZopFLm/ODnqnn+Npb9obaOTOv+JP/16QJm8qbA/c8BUCbTHvD/qVkjpO7TCP+bqcmGQUcY/Hwfg9hQIyT/8Y1TfF0TKPxn2IrxHcsk/qCrtiOVTxz+loEoPfVDEP6R05sen6sE/6PFqhjkpvD/JP+7t00SwP+0kzO9jt48/kDMA+tekor+3Idf9l+WpvyRUYyMtRrC/mgLOPeQNrr8Mj2/tsd+fv5gd8MUPOIo/lj3gjBwksj+UIs9bfsjAP35Xs/+at8g/zQMJIgSK0D8W5RIMIizVP2SUf2m8z9k/uhXto5tz3j/4xe9TF4zhPwtIEgq33uM/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z+IwMLP42PRPzBFlxGjNc8/2m3m1PTPyz+UWxjt79TIP170A/LG3MU/fBddpqXrwj++5Y9CHxnAP44WdUtkJbs/8WVXb2pPtj/BU62JnAyyP0kdLCSOoKs/LkYLVjsWoz9GeBpvQwmXPxKGvVLEzII/yusJmt3mkz8HidJ8UH+gP5RXmrFaEJs/hFWv8FtLPT/G60zjkBGRv9HxhDhuc6C/etT5Fiv5p78ZUldmgaSvv7viGjcX27O/uRP6Iq3xt7+aj8LpMJu8v7v2TXLquMC/pXl1vglyw7+aPjUP8EbGv54IH8pMI8m/UPAue40CzL9pDsrJrVXPv8DzFZbJa9G/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j+5LuBMNW7sP7l0D36Nu+o/rFXRlmgD6T8VsP8bfz7nP3zV2ebieOU/Tqqd+wey4z8jdv+es+fhP85TLOkAFuA/aU01e86B3D9iON4PksnYP5aAH596CtU/JxLGqvBB0T9BtCyYRtDKP+TGbD7ma8M/F0lduwzyvT/ATT0iruWwP2K/idAc5aI/Yj6GoG7utj9QLOfDL9LCPwrlZ1ijVco/mPPwzt320D+mRWpCs8HUP3XIVLTchdg/0w8j8HFF3D8W8a28OfffPxqHn4pw0eE/qFqF0D+f4z/L2mCP52nlPx6VqD4vM+c/WRA+q6r76D+gG+BQmrbqP00BmHvda+w/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6b/Zvfb5FFbov++p1m5I0ua/IxrxM2I05b8AQdBZ4Ybjv5ue8xDH1eG/eBXkULAj4L9yrm/GDszcv6rL+ynkRNm/HO1UkFbL1b+uB5//6V3SvxNN+kXvSM6/mTXNZPn0x7+CAwj1JBvCv6sapsqVgbi/n7+fdqw6p7/fTnPVsGeKP6RpXXPghqM/HX4zM5b5oz9WcgrQNhJ0P5sWPt3Tep+/vzKP/O5hsL+sCPJCA9C4v5PZuOvHe8C/+ku65peyw78IY79hPnjGvx6NH9+58Mi/VZudLvgly7/gyoj8MRnNv/22Ru5pDs+/77w+JLiF0L9wx5C4U4fRv2dtFfNTi9K/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "XVp1BCPP4r8xejBx/Qfhv81UWOiIIN+/WLo+E0pv3L8BiGUWseDZvyDPwOJ5Xde/Mmd06cLg1L+AUcYb1obSvy39V/6uN9C/iWhBzbKxy79AanKTe+rGv11Y6DKrkcG/7ZN0WLUIuL+nXKbKacGmv3ogw/OrhXI/6/ic8Hb2pz85K0qZKwy0PwxT35sSNpg/8hIDpeFBnb+kgG+fZai0v07O21/52MG/vDZDphc/yb9EuhhmOVXQv2uqtZfeCtS/9zfCuY/J179mZoCFSbfbv1C80Ll0s9+/kmwiO2zc4b9sW0Z0VOHjvzMMz0g86uW/t0kahd70579Xh5OnmP/pvw3fxoIAC+y//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8L9Bw9oVTCXqv4EEfc3n8ee/3lXR002+5b8gCe64eYrjvxpJyApmVuG/+OGf3GhD3r+zOQ1r5NjZv7z1wOtCd9W/JM1/JW8X0b/tRZvho2fJv+MkG+uckcC/8+bY2Dmtrr8gv5iiJto/P1TrAJbcMmC/00PTj1FImz8kML/9GTGhP/QOFqLqWJ8/xbKPYpZgmT8x7MbD8/2aP4LSBmcp6Yo/zS4D6wu1mj+MQkcaB6SxP6h2LVnj9rw/JlEU9hg1xD+3QoOHRAXKP8tDAAb7AtA/aRvWGMTl0j97Z7hf9ZTVP20c2f9xJ9g/6YDrW2Ks2j9AaWeTeCndP8BzuOrun98/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "B1wUMyamoTxbW3kOe1yCPy8UVqgrzY0/4n3J/8KglD8m2f98T16aP9PlQrTtD6A/Uth9wdbuoj9KAv2Oe9ClPwPJ9T/OXKs/4qWPMZ7hsD/YZMmmcja0P5O6YQ9F07c/W9GR6QKSvD8oq/RolvnBP4SQAMWFksA/WWyi1jowvj868DvvLzmxP4Hh38OKbog/xZ5w741Jmr+OeoLv4wOavxJwaF0Je4u/SOC7u/Yfpz9CWotn+BS6P23kvsq878M/ErdL+fWwyj+ySsOH0bHQP+D1CbOXBdQ/sn+Bcm6W1z+h6+U6EkHbP1MN2qEH7t4/miohAVRN4T+3hM5KoiPjP5BPZ34Q+uQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6b80Sz5OjhnmvzXwxRV4fuS/nP/zWrbi4r8ku5MqwkXhv6rc3dF2Tt+/m+F6YlMN3L/NTOzPZZ/Yv4XU6M6HFNW/EwWLQ2h90b9u7L29WqnLvxSlNyj0HMS/pr9MrVVDuL+uxIjvHQeav5sHtDuJ1YY/3prAftsckT9u1Vbbvy2fP05jhb5hfqE/Lb2i6uFXoz+8RR82dkSTP+hl80RPp6O/nFXrpJ0utL/cpDzGDCC7v7Pzetixw8C/vim6eQzEw7+x1oUKNK/Gv+QOdA98g8m/BFAiKirjy79HFAvge6XNv9rkbwc9Xs+/r4iHY+SF0L94kDpWk1vRv/sxsNRAMdK/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "XVp1BCPP4r9peTPE0fbcvyF6AZyvftm/arfJHcUH1r/xc3T2hpLSv6P3nrAIP86/vYXf+vVfx7+aq70LsuHAvxUhkV9qPLW/E6wzNv+vob+j9M6+f7OKP1V3zKCyH64/qbpvkQLjuT+1ajtMAQXBP8MUsj07dsI/PoEfxYnJvz+o+0/VzQKzP1Jre/c6NZA/3vjT/G0alb/h73itznqmvyFWHz65laG/Fbsi5btxij/8OZPr4rqyP63+6IC4o8E/QZBYKaU8yj+q+kK9F3zRP1qecdwC6tU/3InFXSdm2j9AAsbDyu/ePy3zUiU7v+E/YHqq9QYK5D8trJr1ZVXmP7APSfFDoOg/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6T+M4mU8O4zoP8cONRbqkOc/XRpmiFuT5j9CG3lZnZLlPzh5Ea8OOOQ/EsJWKVXZ4j82mFFWSGnhP9SGSRx44N8/PPsGXJ6p3D+CbXDK5kbZP0Wnzk/NWdU/spZywhJO0T9Lt/KYCUjJPygSSaOMJsA/4ECvKybnrT/8QTa5RR6kP3R9bitNQ6Q/4Gy/zNIoqj+RtX4WLrW3P2Zj2gBClcM/nAqXkeDlyz+vsHRJhEzSP04jCIH21tY/sPK5Aug32z8OEAwbCPneP2PD/aoXeuE/W+Gt4QJt4z8wzwZOK1zlP1devvEdSOc/l3M+h3kI6T8F0GzF35PqP5vSeuZJNew//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "YFp1BCPP4r/XdKyzioLev6lOOCcVY9q/mFIRL4JE1r8NDGa/WCfSv7quhj976cy/mNs0CCaJxb+PSZF0Say8v0C/66Diyqy/dq15mt8Rbr9yILidFf+nPw/fKse+UrY/IZC6waVyvj8cGo/vf3jAP50lo99CQME/9ueEDPCpvz+Vxx13v+yyP5oy6dmFXo8/MxhTLbTGm7/sb/mcHMetvySTTsps4LO/aoaqlqhPt7/5fqfzPpi5v8pSAdbKuri/nK4PohGPtr/OFNQ+p6mwv5QUICUcaZ+/FzMFLn+PeD+cpPbtyDimP1pMDYxM1rQ/v7GUsPsgwD/6pR73UW/GPyCsPssLt8w/T+kvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7r/dQ6i/VNnqv0CKwHC5n+i/ALrzXmxk5r8E1ru/ECfkv1JK33vG8eG/3vtTood5378FfNUUhxPbv2Ln4kFlq9a/sMxHF6pM0r+mAHY0LufLvwd80mcsX8O/KGjWfxILtr/k0NW+hriZv4iNoNJwgoQ/UdZfLIZdoD93zPwNbCqgP5JsC7JW1aY/w8t3n8Z+pz9E+Ok6DQKlP+QmTsqUOo4/izhwZpocmr+0e8W4OYKzv+aYRBegssC/R++b1iAdyL8q4wKmj53Pv8GdtztaqdO/qdc5YWuF178pCCfPn27bvwe1k9W+ZN+/B+whOMHb4b93xBi94ATkv94mKWKiLea/qPSXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "UekvN+/G0z92oVCft1rVPxielXsXuNU/PfgpT+QP1j+ZTlDdel/WP1h+DTauwNU/ys0p3ysG1T92NVCvWyXUP2lGJZ2xMdM/jlqkwcYK0j9CawKwu8XQP28Z2jFyh84/7U5sq54Zyz92tYO9t67GP9vCRWU10ME/bN9bZj60vj8nScB4k9ixP0oJVLppw4g/0pxjAr6SoL+P8eIYg+G1v+v278e7isO/c7kcnZ4yy7+jS8/VquDQv7M5bo950NO/fvLwgjZo1r+tcA+1K+PYv2Jtk1duLdu/KOxz52xo3b++781GCoffv7nEXlLMweC/Ay12ZwxP4b8lgZ/Ix9nhv68Yc3yhYuK/XVp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTwE974soMaTv89ZLaeTWJq/tMibETB7oL/vhjOo+c2jv4FIJN7wF6e/+GzpC8fNqL/evyU/nveov9LEBbmdJKm/x8u3wnpOqb+UJsZZOlmpv2oFsNKv5ae/xac0dFjnoL+CsA0kbD2Jv3vwH9Rq3W0/yr6TP1Xtkj/g3oOYGM+aP0IFyp88Jag/aOze4obIoD8Sqx3aOCqdv5Q7YYIw9ba/9CA+XnzXw7+9P0VQrArMv/g6t+MJHNK/JWH3SwM+1r+StT+JGz7av+La5f9CMd6/K0vb2IQP4b/qvP1RBvLiv7+XBeD3yeS/NJ2WXTGf5r8QWJ81InPov3H6iPf+Req/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D96pQND8ofrPx51hQ6rfuk/AALhWYl15z/KwCaOBG3lP4TyjE0DZeM/Zij7wn5a4T8CQXvOR6neP9FAICg2oto/x9Kumsai1j8N+oV2SrDSP7emlXwBxM0/uqw7AT71xj8i3w103HzBP1ImooB4wsI/J7wjPyUevD9DSW+zmQmzPwyVIMKW1Hk/4LjlbkVpmb9J4MkxDOiZv7a3aSLlzoW/Uzumc9h7aL+9ydvfZiGBvyC0mI3vgZO/SSwOcWu4ob854uG7N1aqv0cK936IA7K/V2I1dLwWt79PSJ2cFYe9v0qfOYuAWcK/RdfcM7wjxr/TEoah0e/Jv84alzJctc2/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AFVEEw5v7r/RJAHcqZnnv/hDO5MXauW/Onjlte87479VVFq6sQ/hvznGjUIkzN2/j4shlpuA2b8IuKd/6UDVvxbQ8AzdFtG/g+sBVPdYyr9Qt88mocvCv9C1KthEs7a/TuLOhn1Qn7/tFDIaCSl0P36o8DAPYoE/6Guqtgq7mD/iKGmyG7aYP9wHfCM7iKg/1Z2E5iMGqT+eHS4/p5OaP5o+XQmh74Q/6apGz4Vrlz8YrB5Blai0PxyrU8E3msI/BA7jTsATyz8UgqVFRtrRP5akWb8qCNY/oZhmefs22j/gY+8Px23eP/HkntCTVeE/xwlYFhV24z8dh0X3e5flP+ibXEb2uOc//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "TekvN+/G07/QlnRhIiHOv/8/FT4Qwsq/DA7QNShix7/wB26GHQHEv7Z/FNA/nsC/vsOCWCRxur8pFJupjZyzv5pJ75C5w6i/uGQ0wLqSir+I1YKSwVSZP7zLjCnC6rA/u4gL8x0SvD/yC0ybBurBP3ocEWbJ1MI/vnM4ASF4uj/SFgMUcrm0P26EH+R38YI/qKhBmi9PoL9+kK2lI42hvxPR4BgGt5G/2+9krj89hD/iqnWloamWP2/mmr4tGZQ/IMUS37WXgD8e9rYqtw9+v8ZWEr4qE6C/jGOUkHXerb+K814zHDW2v4H65Dvmqr2/ZYjV2NGdwr+dDvBlpW7Gv05ds6dMRMq/VOkvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8L/KAhXaT2rsv/NGF1r/Muq/GHLnvy78579QOb1wScblv2x712w8lOO/TenqJs9i4b/ZSomVt2Pev6AX/Y4WA9q/7aHEYKmk1b9tpgvDkkTRv0Rj6MpAvcm/c+jIlxrIwL8VWqy/LLauv8pN4n1ZbGM/0GenmCBqlj8xezW0dHuiP24SmuYDd6Q/oStNjbKFgT84WQwT+UCvv2PeWDQ9mr6/KKUiyLr+xL+M4bFlhq7Iv7j7YxxZ5Mu/YfhMhWdszr8m53FckWHQv0AdfJFPYNG/bEVdzPVK0r+wwiRiCCDTv2Od5GHzvNO/zq3qMd/Y078wGdw8R/PTv2iPYsw1EdS/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "B1wUMyamoTyUMO5y3sGMPwBk3Ezj8Jg/5DwM0x/AoT8cBl8uK0OnPwwPBn64Dq4/bHDP7Adosj8v9IgxccO1PxkRezCLHbk/5qJC1nhwvD9AeeDO7qG/Pz3iOYHdO8E/iM551URLwj941iQ5PhPCP7UbZT39isE/3z/sOn66vD8NoJvRLDWxP0b4gAiZZ4A/aJV5ZLfelb/pHP9dYpiOvx3LC5dFJ40/+XwP6y64sT/aDug3tknBP5l1cjIMysk/EMdprulB0T9liIVuuafVPxhQfcaYGNo/AFJlTweK3j9VLiCNVIDhP8I1RYj5vOM/QeoMEhD95T8QnfF4rzroP8YhcWHVdeo/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j9z6wT0eFvoP1koUnDSQuY/7tNbSnkl5D/WF1PCKQXiPz85IGwLx98/iPOoPBmD2z/LdUtnmz/XP1B9q26K99I/Hq5UyazFzT+gvCse9ZzEP64GLit5dro/hkhhI8jzoT/0bUgmN7+EP0BVfaSrBpA/OmNtTXNEgT9j0PQdzJKWP+4lSc69Sak/xRzhwF8cpz/PVVNxbnWlP6/P6OV0Pp8/B9xGPcssj79w+I/lYjK1v1bzaqI8DcO/GC5vGAN9zL+249zV8xTTv1QGuiqUkde/NGw92lr+279kLQeuwunfv5fa/ssspuG/fHBIJHo+47/sxsw+18nkv9/Mb4qTTea/qPSXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G078Ui+4G6W/Kv98F6GQ+3sW/sDInN2pNwb/H242sEoi5v8TQzdTKirC/M/3hJ72knr/ERzC5SztxP+LCAVlFRaM/fRSzLsFBsD9Wp8pngvO2P1baaF4/7bw/akM8HVKGwD9nptgNZB/DPztb5IBLgcI/ZO7muKahvD8XCk5mlnCxPy7OWRQKWoQ/7rJB/3sIiL/WjDmxHOecv2ZVuTwIhIa/9Ci4PY1RWb+PlNYC7+l+v8a7J7eqnHy/ujWKz4vvj79uBijHyz+dv/YP0KD41am/pHaYsYsMs7864Psm4VDAv4tBn8WfHci/pg5ENjD7z7+0mLV1su/Tv49vLsYg5Ne/XVp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6b+n7gNWyUXjv/kACiXJQuG/LbXhjYiC3r9D2Da+tYPav4n3uLQBjNa/KyTSM46h0r9Zx23udKXNvyogFYvQpca/vKXQlDKswL/jsn9+BWC1v9bvlghV0qO/8P1Shfy8ZT8W6Y5AQZiRP7pwzNOGGXq/kEfAcR+ccj9IOMhaTtKSPzoNK5z3zak/Ln+uY5Rmoz9s8ma2kzOXP4mKYMuWgpU/ZkjCiCtVpT/o23ZnSoq3PwqomZS8YcI/8Ni0UpoPyT+Uifzq2dDPP0g4ZXb0ZdM/3x+6IToD1z8McI3uB6LaP27Egn8eRd4/NM5lIqj14D/CLSRmrsniP+gM9tsvnuQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "X1p1BCPP4j+wfu4tNkHgP2yIuddP1t4/pE18YQAo3T+PK12tIHfbP1/nlMDGw9k/TJE50EIN2D++ekQbhU3WP4ulseCaXtQ/vVz2BbPA0T8Jr2ZfDTDOP3A4CQmq0Mg/RhznCvDhwz/JH/0EKQPDP/Tnc8u5TMI/Xi6/D180vT/lp96k/KmxP2IH7R/LjZI/+mA28jlCgL+QzTJlXi6Gvw0Ht1aXFI6/eL5wsEmmpD/BHFSg3ui2P3vTxjWaScE/M0P21xTzxj+jiVE+AnzMP4WLVKiy0tA/1hpgkpo70z81TUBY4Z7VP4wfGKTa/9c/SPzT6GNe2j8MEkilPbncP/FLBpL0Ed8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G07+C+T0egy3Tv7YJTAUXlNK/6hla7Kr60b8eKmjTPmHRv1I6drrSx9C/hkqEoWYu0L90tSQR9SnPv9vVQN8c982/Q/ZcrUTEzL+rFnl7bJHLvxM3lUmUXsq/elexF7wryb/id83l4/jHv0qY6bMLxsa/srgFgjOTxb8a2SFQW2DEv4L5PR6DLcO/6hla7Kr6wb9SOna60sfAv3O1JBH1Kb+/QvZcrUTEvL8SN5VJlF66v+F3zeXj+Le/sbgFgjOTtb+B+T0egy2zv1A6drrSx7C/QPZcrUTErL/hd83l4/inv4H5PR6DLaO/QPZcrUTEnL99+T0egy2Tv3f5PR6DLYO/B1wUMyamkTw=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j/hI8ZvNHvuP8LyR8xah+4/o8HJKIGT7j+EkEuFp5/uP2VfzeHNq+4/Ri5PPvS37j8n/dCaGsTuPwjMUvdA0O4/6ZrUU2fc7j/KaVawjejuP6s42Ay09O4/jAdaadoA7z9t1tvFAA3vP06lXSInGe8/L3Tffk0l7z8QQ2HbczHvP/ER4zeaPe8/0uBklMBJ7z+zr+bw5lXvP5R+aE0NYu8/dU3qqTNu7z9VHGwGWnrvPzbr7WKAhu8/F7pvv6aS7z/4iPEbzZ7vP9lXc3jzqu8/uib11Bm37z+b9XYxQMPvP3zE+I1mz+8/XZN66ozb7z8+YvxGs+fvPx8xfqPZ8+8/AAAAAAAA8D8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r8U/SDDtfvgv2JDY6ijxt6/JLVzYd/X27/FHFTOMvnYv6M4rChBUta/Q9UX+LW0079WRpjhNEvRv+2hu2Pgzs2/kz54ON5Kyb/nptr5k8/Ev57nAGglisC/0PSlynmruL/EcXPPg6ewvw/W/GQUo6C/Ouyx3eT9Qb+KZqtVxG6LPwjzCeytOaC/5aDXMrHltb9r81wkbvHBv/mIv9Jb1si/p2XKM1a+z7/HKUqyMjvTv/1HjZhik9a/FiSoHfPg2b/BicKM1ivdv0I9x5HOM+C/kmIuCvjP4b/gaFa161fjv4ppJTBT2+S/+9eoKRA/5r+e8SW+r5nnv15s+v0ux+i/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T+2TR6iBujoPxroggls2+c/BqunNXaj5j9P/UOSh2DlP5cmGQuSAOQ/E9e/y/+a4j8GRkAYXx7hP8Nu0xutPN8/c11MXREh3D/i70yWhP/YPx5BAWl/ydU/KMVEllSQ0j+QuHbfU5vOP/72PGYCO8g/AwktuDTRwT/lOVKVG2K3P1qj12wBjaw/cIWP0j0Lsj/74tr/i2a4PwLIO0IxJr8/LPTtQMb3wj9oV4lUArjGP090ZXSbgco/z0AEbIBozj8h8NNCvijRPxm90ca9M9M/0pF8oPFB1T/aHixHfIrXPxj4Ho0v3Nk/Y0a634153D8EPrt2OSffPyWQPvvgE+E/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z/iVK5NhxHRPwGYX//+mM8/S5+6eVYFzT+Wr8OrSm3KP/++PKr608c/p9xz+KCGxT8Ha2VPgT3DP9rYWmPjEME/LituIyfWvT+BBJVVF7e5P8fi5WFrdLU/QyxtJjWlrz9EFIgyLQ6bPwQElJK+qIA/wp9v7qJ+nD+aDpW2nOOhPypY+2C0Y6M/7GG0bxKEpT940jnKJPixP15298WHMsE/CCyP2uJ/yT8wpqxLUb7QP65plq+bjdQ/Dx8vRCF52D9P8LVxf3DcP6o1fm5eNeA/BfDW5MIx4j8hEFrFYCzkP6LkDr2PJ+Y/3dZ/MhUj6D8HO4qVtx7qPzgVyh1fGuw/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j82GwAq3cXqPxKk/mEx7ug/5u0Jv8AW5z9YcQrQgD/lP06ulQqwaOM/XXIuSYSL4T9UMS/ac1nfP+niq+8ymNs/pgeeWjHX1z9Zfqor5RXUP39d2zZYTNA/FAYdMkwEyT+Q6Mn4JPfCP4Eb4Fw3ccA/rV1HDIcIvj9zdzc0rY+xP6BrAtxqxYM/cBSiZhpZmr+LxDiUYmiiv3n/OzvXTZ6/s3726b7nlb8seX14HAmGv1AL6j9cc3W/TAAbvKt0br8wrDJrlG1lv6owZwsBqFi/ngTwcR71Nb8OED2Os/lEP45zD3AVb0Q/Rrmll3wPQz8dVi33QC5BPyh+NdScsz0/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6T86H4P1vznlP+P+uTe+peM/8HVKIpIO4j87n/FhtXLgPxvbaAk8n90/IVNthUlB2j9LfN8Yh73WP87xe0lx5dI/gQUL8jkjzT+JVezh/HHFPyjSF1YRl7k/i3F+WoVInD9oIWiKXd59v9CNcY9orH2/Ypmm64popD+L3ua9mRWdP9oPfI+JFJc/yiiWsfY8mz/DkeiMyVGqP5Ad08WfQow/DMNPcycvkL8Zsam63vuyv/4nHcIgfsK/zWIrkicizL+qp+UNqK7RvwlBG3G2UdW/IvNK/l1K2b/M8qGHZ3fdvzoy4Tfy3uC/FfDzX00J47+M3pcbPjjlv1x2w3XRaue//1REEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "YFp1BCPP4r+ScC2kzWfYv5nABXA5itS/bHeRFY2t0L/S3fTL/abJv/OKuv7h+8G/XE9dIZ/BtL9FRtFaZlOXv0XP7/EEnZ4/Lnpdchwfsj9JTvE0cP+1PytncmAak70/D+eya5XKwT/MlD3FkQ3BPyeJiwgmh78/MkIoG5jGvT/H2TvVkUSsP8+IEHUCD4E/Sq9vIRLkor+/1resAcSZv+GjFGmKlJS/EAlRcYZikL8VNHZ82XZ+vwnOc3fgpYW/hUs2dijdkr+al4uyh3qSv6ZXQ+ex2XY/sMpfKhezoT/izXCHwMSwP9nsNH6z7bg/b20NT3KdwD+ZGDcZmc/EP0tU1OaBCMk/UekvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AFVEEw5v7r99Ukv721Hrv4rMWFjAPem/w934SoMp57/SSPKb3xnlv5KSmA5XEeO/rNzdUpsK4b/WFjVvpAnevzRxVooyAtq/vLYo0+YE1r9MfCIqgizSv8Qaobuo+My/foLJ6g4Uxr8/Jv7Ge1e/vwRVLBJASbO/+1eB+SLDl79zKPzcHvSWP206QMR2gKM/MblJgs3Roz/MhBCDSdmVPyRW/LiAAZG/nbPdIWVyq7+mAnC+yay2v/OT0wrbsL6/vCO+Z3Lnwr/nxPhJ8qzFv7DhrFmeOci/u+msrcdvyr9C3wpZZILMv8++iQoFhs6/l2MUQa5F0L/f73dXnEnRv8DKmvEIT9K/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "TekvN+/G07+yzRV2e9bSv4EKSivUC9K/ZikWp/Q/0b8ZTs6/mkfQv3KlpsDPL86/MtH766LBy7+D27PpAD3JvyHP0bQxjsa/xXh8F1C2w78aN3OClEHAv+RX7+J3b7i/8J17Rsktrb9HFVI/T4SMv9fTiC6WyaA/DI/vqrQysz8JONoikFGzP2skkVkrwpE/nN4EIR4Fnr8Sc5pcmYSwv+J5Efa3/r6/J4cnDNcZx786RQJwmLLOv+XTF5BpGNO/KnXnXi7y1r/gdUC3ft3av15AmtN33d6/iEPiC5Rw4b/3L5segnrjv3zTop1thuW/na2/G9KS579KeykAqZ3pv0u1TQZKqOu//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6b9rGm8kQXfiv+fpmuqHU+C/XMgOnHVn3L/kavSpqzTYvyHaLX1yG9S/PgH4Z+E/0L8CuC+jf6XJv6sqHyWNRMO/VD32cuHlub+igBDB8xurv2Soqc7nDYO/vOWOk/NynD+A5WM7YDhDP/A3DjmzKGk/1G4/kf09ez+7nQ0jwNKiP240n74zh5k/TvKcNjXinz/JNJHgqQusP+eT/j6y4JA/3CbDWIAglj+EIfJQVtewPwQSd64zjr4/FhEPWMzaxj+dGAGiAxnPP6C4bcOK1NM/bk7tZNsm2D927RXEgIHcP3hZj6yaceA/oySAa12l4j9F0HxQrtvkP+bN9DmtFOc//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "X1p1BCPP4j/TugevnmrgPzHnUFrEPN8/9606clGk3T+HixG78wvcPzdber2Rc9o/f7Uj3GDQ2D+c9FRFQd3WP2E6pZEGbdQ/WBKtBx250T8hHPUBbgrOP7RZZjRAgcg/WaFpHyJiwz+qoC09Wo3EP9AAcgHDYL4/TJJbHBIvuj/+AejUqTmsPwAqfW8BbGg/6Eqx2jvFpL8p9JFoL2OZvxr2zoWPTIu/2rJuYpNnm7+J6eHty/6ovx/xeuv+CLK/u01uP+oAuL/s+fAw4aS9v0H5ArExQMG/974kPBWtw7+Jyl7bTBjGv24gwIRVhMi/3i6xOiTxyr/4sUfXhF7Nv0RhtY1rzM+/VOkvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j+ESEWoEE3oP2JSWfpOMOY/R2RvQsMW5D8qLynH0f7hP3Uu9wX/zN8/+Go49viX2z/wrNd191nXP23z8Eh4GdM/MT4eat2OzT++cEpE1w3FP+TKNnfF3ro/DZblZz4wpT9AGjZ8mP55v4DuRKqQl3G//CWLAmGRmz9Bn1fNT0+UP3L7dFaiT6c/GHCfYhdloT8YmQOiGx2mP/I3P143Opo/bOQTHo7vgb88eSSnRTu1v8QXc5P6hcO/69rhlLkUzb+vuWyz9zXTv1sR3ebnVte/i5YgQU2b27+B8mgasOTfv/h8III/H+K/XBcdeFhk5L/Ig+qKn4Xmv8PDZYuwqei/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G078zsL4wOPLKvxP8u824yca/VcFi2Iyrwr/Efm4RlSS9v0Kxhtd9/rS/aTU6uS7dqb/HalmGUPqTv2Dk/+q6p4w/tDi07KqLqj8GSot464W3P46PNV0Jmbs/2CKaAxq6wD++ZqOQPiXCP5Iszpe2hsI/Cki4h9gJuz96Eo7oQI2zP7iAvvTuvHU/BgxGQgvEob8NxhxgNdmgv86O2dHl/5W/Ypa98+agf79seFPV5fVzv+uU3AYWM3y/Zq1WQl4zjr+scSRBc4+bvwrCDtXe76q/ye+S1AXes7/K6uSMCza6vzYbMuMMhsC/4E02Of48xL8jE3RCIfPHv4L1rYHsnsu/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G07+hbRD815zRv3bCuFmuftC/sBUSkIm/zr+4fA5GWXzMv8JGqi7WMMq/qlL8hfXXx7+FItebrA7Fv7tTuW6+WcG/ukJeWIjjur/4Zl5tJCKyv0UhOAYsF6G/XOnc1oLQeT/hji39F7yJPxDUeStJkIE/SrAlQvrZlT8YvNM8zpSeP9XO+62sPKY/t+WgLVCYpj/m9qrW4nSiP6ZwUs5pAqc/yho+lUDcuj+ceqmiK2bFP+NynB9SYM0/iUwORiSm0j/kyq6/gpnWP3Z4uTTvito/4VF9GjqS3j+aCYpIXlbhP3DbbwjDYuM/URsuWHhu5T/IzEXArHnnP6yDwSzKhOk//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j/3AH3KH1XpP28Q3cD6W+c/MUz7oIhg5T8Um33jK2TjP3IfKrOYZ+E/AWiC6fnW3j+ctH17mPTaPyPw3k3bSdc/eUJ9cnOk0z8kes74fCDQPz34W9s9x8k/Eq7VoFI5xD+qSPSyyobBPyZWKWuhXcI/X1LSmqnqvD9vtxBhrC6yP7BGf+dbI3k/VksX5YGNlb8ip4diQBefvwDxYktORwU/Xjsy3KWwoT9kgsk27pGwP5f1BBJYpbc/jSk+9+R+vj/HWIgr5o3CP2k62G05zsU/OXHmLcmFyD/FyhWNlcrKP/LJ56yVBM0/3RRIqzE2zz/DfwAHFbHQP73mBOLmxdE/T+kvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6T/sNDZNy9rkP2dVZhhM2eI/BI4WDMvX4D84O7HodKrdP7CTM4yUoNk/TPOBUQiV1T+LB0B01tPRP1VfTPwKpsw/vXtucm7NxT875t462PG+PwZ+zSepQbY/kN4QAC1Dqz+soVuwY+uQP54vzH14I4I/+hUXN6Tolz9Z4QVApT2lPxek3NVaf6c/3XzKIz4bpj+sNvYYw7akPxdr8X1Qp6c/nnX1+RJxsj87tvie0pq4P/mJiRZbSb4/yhdJ3pPawT+fBsXd5X7EPyZnXtpSHMc/sHrJFYaDyT99E7x8iYfLPxKDIDSFhM0/yEexiO91zz+HqAz42K3QP53ZvCRemtE/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "Xlp1BCPP4j8eC1CdhsPhP81uAJmrIeE/AXH+BJx94D9ciZ2E36ffP5vdlvT8QN4/ipg2vfW43D+FkEqBE6LaP/ahLVa0Hdg/BEY8j/th1T+8Zzrk+m3SP8YcID9AOs4/PiO6caUVyD+qH+dbqErCP1wmOiqeH8E/eqzEJ6bkvT+2D24a7E+zP6XkrP9ej4s/nOTPzTBQlr8UUasKKT2sv5CKXwxQobe/L6QQ7BKgwr8yt7rksqjJv7l1+flJdNC/D6QRM8Qs1L+3NQzV0fHXv5W0E7CVv9u/w7g+bn2U379GlgTxULvhvxkpaGmxreO/cTFz/lKg5b9KToXawJLnv7QcMqMghum/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z957rHJcJvRP/yVyTinstA/1B2OnGarzz/T3OVCnP7NP9M3iYaHVMw/mSonXVKqyj+rPvDgnMHIP+LzWjQZIMY/H0v/Ax+rwj+EjbXu/+29P6trIVZ+LrY/dkMp7wZRqz9tjAaaq66PP7Nn9kcjUYw/hqq1ncOZpD8Kq2gu2cCUP943lkUZjaw/XbWejPO3oT8MxDSLBMmjP0JrmW5Pgqk/ldYXujOxsz+kIbfUqoy6P8rbH1dKd8A/NlBTZFqjwz+lWWaTKJXGP94lnH8hUck/LQ7xhSM7yz9ebEYdbfbMP3Fz6u9yps4/yM7jvcco0D+nf61YOgDRP+gZZVbb3NE/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j/PvBuVyz7pPyoM2NupSuc/C0b5fk5b5T9acHvcN3HjP86sIbqljOE/YMJzKY5f3z8AbdIOvLPbP8HugBvtKtg/yX7cOtLh1D89guJG/fjRPyQUM0S+Jc4/Jz2zrhwGyD8OoueHJY7CP1vcNmbotMI/3/JGmOg+vT+uokxL3oOyP932EmH3jIM/d08rZcrnpb9u7LeNT5qrv5gsja/DhLa/uXWT83gWwr8p2qgIQuPIv/u+BME1uM+/+JYIxYpb07+eKGE+LgXXvw5xTmYK0tq/h/K1n8e43r+u+N+BfU7hv7W/pXJDQuO/0zawbk035b8A6vcZ1S3nvxIQcJ7OJOm/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z/iVK5NhxHRPwGYX//+mM8/S5+6eVYFzT+Wr8OrSm3KP/++PKr608c/p9xz+KCGxT8Ha2VPgT3DP9rYWmPjEME/LituIyfWvT+BBJVVF7e5P8fi5WFrdLU/QyxtJjWlrz9EFIgyLQ6bPwQElJK+qIA/wp9v7qJ+nD+aDpW2nOOhPypY+2C0Y6M/7GG0bxKEpT940jnKJPixP15298WHMsE/CCyP2uJ/yT8wpqxLUb7QP65plq+bjdQ/Dx8vRCF52D9P8LVxf3DcP6o1fm5eNeA/BfDW5MIx4j8hEFrFYCzkP6LkDr2PJ+Y/3dZ/MhUj6D8HO4qVtx7qPzgVyh1fGuw/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j82GwAq3cXqPxKk/mEx7ug/5u0Jv8AW5z9YcQrQgD/lP06ulQqwaOM/XXIuSYSL4T9UMS/ac1nfP+niq+8ymNs/pgeeWjHX1z9Zfqor5RXUP39d2zZYTNA/FAYdMkwEyT+Q6Mn4JPfCP4Eb4Fw3ccA/rV1HDIcIvj9zdzc0rY+xP6BrAtxqxYM/cBSiZhpZmr+LxDiUYmiiv3n/OzvXTZ6/s3726b7nlb8seX14HAmGv1AL6j9cc3W/TAAbvKt0br8wrDJrlG1lv6owZwsBqFi/ngTwcR71Nb8OED2Os/lEP45zD3AVb0Q/Rrmll3wPQz8dVi33QC5BPyh+NdScsz0/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D9uuNhACL/tP1uf1P2Czes/k0/PiqfZ6T8WYn6tZ+PnP1jrPZ/s6+U/ekg0T8r04z8+nESgCP7hP3sWmaj1COA/Tc7Hd7Yu3D9TGLIeolzYP4QbmoYalNQ/Yy2DlP6x0D/VqcuCDGfJP0vRE9d9ScE/Qf2eSw+ysj9UZrYSSxCiP0OOdeZKRKY/IQ1Mr8Emqz92hi/BO225P389xuWc5sM/hLljG4qJyj8h0rmS52zQP9AmVmqietM/K3rLtKpw1j9hCzbyP2fZPwHpt5mvaNw/s1N80oN63z8TzK061k7hPyC+kWqd4eI/NFy8+9Z05D+cu641KhnmP2WmlJ3Gy+c/p/SXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAAArOawo1IWRPxP1Qo4uD50//1Tpn8Udoz/OSvOzCGGmPxBAZhobh6k/HS6ulfuKrD91Jy06YWevP5nDBuUXEbE/d2skNjvCsj9zhq5Z5tG0P2K65tMd9rc/1t0mfY4BvD/eC0iXlZm+P77hjXtmeMA/4vo43h7Lvj9iuiSb8Ye0P16sYN+BY5M/qfrZag+nnr+GElJ/zJuwvwdeePgHbbm/t96JbmOVwb9kXuhfNp7Gv6exYVuVv8u/zHGLUiGD0L/V6T2DNRvTv/9A9mVYqtW/frRbrPYl2L9bE83L9Izav48zUsZV8dy/z2JrHepS37/I+c1wmMDgv+a0YZvywuG/YFp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j8GwHxwZv/oP5gx+bPA3eY/i0xBf+665D/2Qa8Jn5biP4tesSqccOA/LWAC31CQ3D8nvF+frzbYP3OzY2T9vdM/DILBNBZkzj/0SRqh0FPFPzgLiOf/kbk/xBWd6353pj9uduhbHJuLP0q9IB/k7Y0/ykIqyvqEkD/2F6nR812kP9+nU06UcaY/7qdnsE9Ooz8OpvBLL1OUPw+PC1zoY4s/IlyKTNRtlr8K2OP7REe3v0qyvFQP98S/b/C3q09Lzr99OL3p5J7TvxGbMCrG8te/ZTMBQ9NH3L+P/YoygVDgv7ol9MKvdOK/f/H3vw2N5L+zn2d4+Kvmv02oMRPjzOi/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z9Quc5b+gzNPyCTl+UO28k/rporzOuzxj8BboCUG5jDP1F3OFp3isA/U2cC/Pwhuz+pAXNryna1P6ESoprLIrQ/UFIst+Q0sz+DQaGtNIq2P1qgLd8QFr4/dBSay4NOwj/ye/eta9XAPzU11rzrJME/mT3dkopXwD+Q4B53+CqzPxbN1RZCdpY/mr9nhO/+kb9Ot0Huu+2dvy4g4E9jlHa/sNS8Z/0Ogb+7ZeraLXODv3qpJYF6Aoy/tr9ug1W/lb9fdPqUX2Wiv2ghp7AAZay/sK5/yB8ptb+WfrLSmx68v4M7IOZzlcG/MTroegMYxb9HLPTMRqPIv4/0vB5+aMy/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTw3/3i+sh6RP3XIFjzD9Zw/S/4uI4isoj+PfE5qGvClPwMDEz87Yqg/bDuUDeVpqj+KZruIwFurPyBx97xTzqs/hQgTjgpArD8gsqHroqWsP77HxV5kIa0/ALaEQhJsrT8/uVWZfU2vP6ag2qU0K7E/ZSA592sbtD+Mm/lzZ1C3P2SvhyoazbQ/z7XL1ndmvD+gjJapKPPBP1IXtPzVs8U/JYfuzO1ryT9DHuZJwxbNP/M7JgHrYtA/HiMJ4ZY60j/eE0f1jhTUP2EANjje8dU/d2uetIDc1z/p7AQODuPZP5B6MCka9Ns/Bnotz9cZ3j8dyDbiQCvgP7oW3vcTX+E/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D8I98UBYyjuP7ozqoFqfew/vQDh66LN6j9X2SzCNxvpP0dWeRBmZ+c/rhTfhM2y5T9+TuS3Qv3jP32a3bRcR+I/enZqzViR4D/WdqRnOLbdP5AD8fT6SNo/LEjH5gjc1j+mlw6g63zTP7gvM0dvKdA/PddOYN/AyT8qxf/kyDHDP+tUlaeq2r0/1tetfyOWxD/TWd/4okTKP5z0WXa5888/kRnARTDX0j8Vh5sq0LrVP/PgCjqyndg/5+caZrKA2z+YOrGmQWLeP5OYWFjcoOA/i1FoWdcL4j9NO1WM+mzjP2blJU/syeQ/mvQxnbse5j84hJI9J2nnP2QC8bsyoOg/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8L+/KsGBK7vpvwcHA5WMhue/quViIUpS5b/uR+XVXh7jv+4bFn706+C/ZtVCC4Z33b8x7QjEHhvZv3QeqSvZxtS/GhDUsAJ60L8vSty/vGnIv8R1hO+Dub+/RJlXUwTVrL+kKjP8ACZ2P0AAuvcX+FS/XZ41UaUIiT+CBHRZTZOjP+Aee3blf6o/vNR7ZkMmoj8ONca7eMCjP/Qin/Cpzpw/agJwKLupmT/cpKord3WgP4eSs/UWeKQ/AyP1ds05qj/d82Bh71WxPwhHxMNIS7Y/lHrtYUpzvD9m9IwN2GbBPwB4wBxBoMQ/cruHhMTfxz8p184L2SPLPx1jfuKXa84/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "B1wUMyamoTwTxwYG+RxQv+P1ZseBcmA/HyQCJFvrdD9THOd6kiWBP75WxjxoM4g/VU32N5Hnjz9UgPTGH2OUP0Bwp/6tV58/rzZUsq/Tpj+N0RL2K7+uP79mtJSRDLQ/UwR+feIjuj8X08wmWiHBP+ZGsb9nPsE/aqe+BBejvj+wEaAK+sC0P9A3eYhfZ5I/NMoqZjYpir9suxpoooWivxaCY1UVjaS/3joI5HNCuL8mRnEKPzrEvwugUpXfOsy/CFDXqzcF0r9DBBD7GeHVvyUKqhY/ytm/PybIFZmm3b/+4kSQWsngvyq+wa6RxuK/r1Vj/rvI5L9mC8DHxc3mv4YqjZUk1ui/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTyh1londV+mvx7IyiXsBrG/lBz7zAumtb/XTqOXAWK2v/A+TPWh9ba/6weYQYgjt78Vhurkv6K2v9Hf3pcxarS/R8tSZRSYsb/dlKxzEc+qv9XRwFKWbaK/EE/UrVmYlL98i8epsrBhvx7HFLLRuHs/PIat3TqYkz9eT64SYPCdP6K4h49E6aQ/ETcU282tsD+BCyCxNFu2P9x7hZ1B1rw/FZtpOiH9wT8dJHcqF5/FPwAlRddcWsk/vfD4V2EczT96jrTRuCbQP7ztyzoKntE/Aq0geXvB0j9yN5eo88PTP7XpHDpfstQ/8HdCK8eY1T8aZfXi14bVPyqp+OHTM9U/UOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D/1xr/BIuztPwYb4B84Hew/JSKtUvZE6j++vX6ifFLoPyIUoNzTXuY/Dszz0qdp5D+2/VaVbXTiP3MQVOMhguA/dxo52OUj3T85qUfo0FLZPxVW+0usetU/IXGxwoKX0T9B6/fnwn7LP6T3LR0x+MM/DD5W1j8uvT/dSOuJb2ywPwfOyPr2j5Q/+684vSYesz8H+4o2G77APzCLhM/l18c/WNd+px/czj+MQ+hlze/SP52KFio4atY/Q6OOgRTh2T9FAKq2WnrdP0tAWsH4kOA/878CTzRy4j9Ww44LXVfkPzxjfQPMPeY/hA65+hQk6D/vbMZ9YhTqP3mwlBszBuw//1REEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j/rJEM7s3vsP2oocyAkGOs/Ah3O77Kw6T+ar37fbDfoPzQ14WrscOY/yXjQXAKa5D+kmEW4mrbiP5lTLNFixOA/9vuVvS5x3T8mgHHz/y3ZP3qeknKZrNQ/pWilxHUu0D/nkj0fj3DHP300wBsCjr0/9PHFiJaqqj8r3FL85pWjPzH6NRU4o6Y/A1Xcx/UsqT8mERY21P27P+C0GMRH9cY/JFOSBiX8zj8DMObC76DTP2wo7E/Xvdc/vx3ohSCU2z/WmhBcBK7fP0HFJtE1yeE/5wCKgZus4z8h5DrnrojlP4Rsqd/5Xec/n8sxouII6T/goVuv+qzqPxMP6FWQOOw//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G079Sxid6uTvLvw4+bxZGX8S/qMC2Vh8Su7/xYU/vXAWsv5yvPyW4wIy/sOIvyfefmj8f80GiI2SwP/5lur0ehLk/67V/S0S3wD+IdHo4ncvDP7IiF8f3csU/VKdsyeyAxT+zHLsiIODDP9F7xVnYgcI/5EoAdtHsvz+fEB1lb32yP3RuqI/KZJA/BPCEcxcgoL/aTZ3nlLOrv4v5XSVqmrG/ZWaVa6ATtL8R054Thv62v44RNhCb9ra/PGKXdUR4tb/qa1kKnkmwvx2sk9zBX56/NN4S0TF3ej9q567amkymP6eDqHtP1LQ/wmFYfzMgwD8H3qqF43DGPyy6nHuVzsw/T+kvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTzHo7yk+suPv/t6AHSoDZa/Mytdm7wcnL8DQX9s8Ayhv4SAqn1E5KO/JAtkNdJPpL9aaSxT7kSkvwTIzD+UR6S/ZHPUY4FEpL9WOkivOiikv1Z555Jtu6O/io/808cZoL95xNVxF1KMv/76QVWcgWU/YZTnLKR7lD8KTbFr5HKhP2vSs+D3SaU/oVaKZLjBij8m0QZsezKrv0jQEmOZOL2//lA1clz7xr/M8TW/Q0jPv2ymlTPFztO/iQB8yAD417/YjqNwhiLcv8PfN5/DIuC/eGy/o6Eu4r/l+yjQVDXkv+TdbfLmNea/tG+aLYo06L9BgnEiCjPqvx/WjpgRMey/AAAAAAAA8L8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D+40/u93RDsP5+lUqA0Euo/LUebC4cT6D92N3eoGBXmPwCGj+0eF+Q/FsYt8Fcb4j+nxvGurBrgP1XB7wfVN9w/p6c+pqc+2D+zzQXPQUrUP67uUvsfWtA/HxCho2/byD8KNXyAEd/BPyA01CAQLcI/TgQY/gwJvj+EYiRyBxqyPwe7LfRuU4Y/QfTNokDOlL+yr4CIMh+CvxS+iKxqtZE/VTA4TjnEoT+VLyyCzc2nP6kIUOL43ao/8usWO+ZLrT894Gdo6yquPyHXAXJ6aK4/jp+mSNberT/DCYof3jOtPygQkAJ8Hqk/0K9n+vSXpD9OXFxJGwmgP57kgEwl55Y/B1wUMyamoTw=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D/4B6RCgv/oP6PA061KpOY/PuP9oH9S5D/dHURICf/hPzYPRtWTe98/vORAaGMj2z9c75IwYfbWPxgaFlXX8dI/6cvWKS5hzT+qIVs+c33EP/J69ujrjrc/WE91ca8WoD+UrE1K0vmAP39CYPeRxX8/Rg5oOWKHmj8cyk0OEb+hP6prGUBdwZk/8BESw7bdmT/8515TQ2CmP2vHuT1TY50/DKJkBqO4fb9iWEycpCCyv5xbfQ6t1MG/D+wh00X5yL8pO6EZMRjQv9PUQkEJbNO/8N8jMnPO1r//fPcpAUPav1TPSbInwt2/3gjf6Tmk4L+Dp737jmriv8z7kypANeS/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAAAkb9fyCNF2P6CLudPtX4M/NkmzpweDjD9YPZdpxTmTP4OMjmR3iZk/RzHBMp4JnT8pk30LTROhP1NBcLPKpak/Bmr+lNwgsD85HahdLiy2PxC3YJa/Pr0/SME4ZboYwz9gbG42MpPEPwR/4F8KFb8/eHeK7bVYuT9KTNspcLKtP/BLxqcxB5E/2gqGvxZrnr81C+YCexqjvxA7hX0ix2A/GmZU6/f8c791aOwtg6l1v3rehmVhInS/qItLkRRrlT80K67F+DetP/7n/nt4Vro/0ChDEZRLwz90Sc7VqJjJPwcbywXDANA/Di+RzFw+0z/RF/W3WoLWP0if94o6zNk/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTzQFoMDfOqkP8BGpiHbNas/H7xYgoS1sD9U82KuUL6zP8A4n1sFrLY/o2iWndt3uT+fz1gPgwW8PypvMRqPb7w/BoPbHyxLuz+TQ5mzQcq4P7Gfxvu3E7M/vmY6G5DQpT8sLjTfs1OLP0I6i9XLvnY/eMrSox3voD+GRukSs6qWP6IZt+yOU6k/Z5Yz4FkJoz8KU9lnzamlP06TgRg8WKk/clYIbwrGrT/vTvvkP4itPxJsC2sJAa4/rmDXFSKtrT9gUA0LqJisP2gugZ0uVqs/xjmXlYXfqD97zFzjdRWmP/HiEPRPMqM/ozewS+kzoD+E1mVex+WZPy+hXgxSaZM/CoqeTDl5qrw=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D/WpadO4LTqP6M6R0rPuug/Dqp+28vA5j9y2bRQW8bkP+g7ysIAy+I/A+piA9PO4D8KBXPpz5jdP5T2Verzidk/xjqs9meU1T/A8mj/4NvRP058BJUEWs0/oNvH5ecaxz80FM0AvU/BP9mpyaXFbsI/UGnFHFtWvD85LKejUFOxPz79WT22dno/tb0+WJlrkb8+Ud9uQ+Kkv/9lgOdlB7a/Mmnk4Ljywb/MXvTf2RfKv4oGNV9DG9G/pHB1sP0s1b/45KYccjnZv+kIYSBmQt2/25v/QzKe4L8WxADpi5zivx9QSD6SneS/95fEdvqf5r8V9lGwoKrovxDLUmUzuuq/AAAAAAAA8L8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6T8PSksniPLnPxYRoQgwn+Y/HWvtqg9L5T+EP5rWT+zjP7WQF2C1h+I/iPKWOdMk4T+cY+IrAI3fPwGO3SwQxtw/VrybgbKK2T+VWBTAhbbVP2xKt0ACx9E/Ka8/Ci9yyz8GeTvPmwbDP30MzQ4FpLU/nmfmlj5roz+K5dSVe8GkP2qmlIvIxag/d2C37NGOrD9+9VF7JYy8P8+DUfeIrMY/8cXHqOUmzz8961C3sKXTP7TnM6MJwtc/6DWaBxOc2z92jCgMeAXfP5cMK9qTNOE/ElGORDHs4j+OPudhQKfkP4e8QiEwZeY/FFTGqeMn6D/UD8A42/XpP4Zlf8r4xOs//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "Xlp1BCPP4j82+zlbZ7rgP/lIpVxnqN4/j93op6nV2z+DbeB9BCDZP7iTUxLRdtY/289GzrjV0z+ShyfX3T3RP4G4yiR53M0/WrT3tvHRyj8duMVlfO3JP7J954Ssvsg/K7/tTcX9xj+qdMSvfILEP5NBAwR/5MI/FEbpIookvj9SI+Su4SOzP2Lpxek5PHs/er4HT2Wvnr/5azVP/lSsv3k0UlCjDbG/lBV3t7rCs78DHp+0sRG2vzgJ4FoGy7W/0ufZQW6stL9Il3yMBF+4v8aW23Yx676/nSrEAbbxwr+BUFqj1XXGvyvetJq1/sm/G5MO/rBazb8mrTIriTnQv4A8Mt19xNG/VOkvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "Xlp1BCPP4j/d5z7QkKXePwNBK1HP5ds/bBBm4Ykv2T+qsyMSXnbWP8fyl7qcsdM/2IDE53760D+0wv03+x3NP3GtHBX6tcg/I9cso7tPxD8wL4/Uv/W/P8zoQoAVdLY/kJ/jBwMrqz8Eu7dDjVySPw381iKWZpI/6ujOGq7ImT9ZBdiE8xehP8r2Tmq+i6I/FLAmSBHypT/EmttzpR+pPx3JIhE5ma8/cBmN5kcOuj9ci9WetmDCP7tbYFSHxMc/eDRgjpfZzT+pu2sYQQHSPyKUetDQJdU/lAvfaTBk2D+QoPtPgBLcPy51nwSOv98/nTdR6Iy14T+Aim9JH4vjP2uvSBkpYOU/p/SXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T/G+PNovKHmP1WblXHnN+U/pPYMCCLS4z8jq3rNi2ziPyyzRdL9CeE/qvnxKpg63z/Z+gHpFyXcP5YUJJHI7dg/rU/n5WOz1T9vlUWAZ3fSP+d3fB4bis4/SEH9R7ZayD8jD+qyqdTCP/6Fb0yNDcI/MhVvk4Llvz9xba2csciuP1qYWOlUfJQ/c2TURg9zmr8OvybGCfqrv0+rJG2SlLi//CBlNQwgw79kJRr8+HjJv2bOsB1Fmc+/CSv+K9eV0r8u7363MEDVv/mCHttxzde/jwwkbjwr2r99IUPgJOfbv4kb8ISyl92/0s5QX/M/3790kYXJV3Lgv7g1BYNyROG/YFp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6T97VFKpivHmPxK93HzdXeU/ser0Q9bL4z/J74TLzDviP86/ebwIreA/uedA9nFD3j/DQHr0YizbPwekzyrlGtg/qGa5fMoP1T8rv5b/URHSP0ismWqopc0/DHCwvcmGxj9EroGm+dy8P0AIUCsTMao/NIM34TYroD9FzpHNPHuhP0iPgjgSBaM/HZef59LSqj8+zxg92Ha2P+z2sDu1IsM/kdAmOatzyj+IIVxMiIbQP5TDgPdxktM/i/b5wBuE1j/2Q6aHCW7ZP9gMuiBSXdw/HN7DMK1L3z+6POqdyRvhP3R4+/5vkOI/2BQlvKID5D+odP4CfnblP2UdA49K6eY/p/SXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "Xlp1BCPP4j8y8JjoTsPgP47wfFSZRd8/mcWuxRsN3T9uUDAbDtraP+CWO/awptg/ErgN3eNx1j/rLolYIELUP9Pb+H5XHdI/0S3lR08S0D9LQ2lXvHzMP784QwSng8k/pKGGUc8Txz/iZ0iUvEzEP5ft031QE8I/TOcxVhqavz8LUg170umyP0XJu5MKiY8/LjGUFEBPmr+pJF4Evr2sv4A9OTSSY7O/iUAhVJIeub+ywU0+yZHAvxMPOtO4H8W/3j+jIxjWyb8MFHzZy7XOv0dHT9xV09G/3gelBO1M1L/Dwg9vWMbWv0zRJzgRRNm/Zkeb/o3A279pGOnKMTrev7VUQIdRVuC/YFp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j+ntds/vCDoP/7x6yOkCOY/od3oyXbz4z8Cmd/2eeDhPyfqk5oknt8/zO2s139+2z+sEhYlw17XP7lLZpQQPtM/fJCAIo3tzT9QJYlbvljFP1lgslQfKro/QCRMJ/8Rpj8Sa24RzG+OPzhW2qRPwms/KpLMX0Uxnj/65mC8CwqWP2UjCl6Px6w/3Hy78XHvqD+MsB5Ze8ekP0Upug0ftqA/0kXkdDsAf7+3U9DHk+ukv7DNYFfDeLK/KMv4qRNQur9et+Fz6ZDAv5rj54DzLcO/irHyzyS1xb8Eju7rWzHIvy1X/fOeocq/20T4IoIHzb9F12dOH2XPv07ctx0P3tC/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G079hkT246dXJv8sBkG8IbcW/NQeB7i4Gwb+07zBv9kS5vxejMe+LiLC/A4XTC4Z9n7/wIjd3k7JjPxaz0kgtl6E/+072kzqbsD8B0OJc2yS4P8nmsDpR2L4/91r/2piCwj+lrGvm/ObCP8hgcwlwncI/e+cjMlZkuz/F6ZVb1a+wP8sxZfq4K4U/lkDttWFQi78tW13r6TGiv5dCwVGK16G/adIm3LyJnb8I8pTC7yOSP+KrsX8V9bQ/mWHGpSAAwz/ZavZ8t87LP34PuZK1eNI/M5dxepcN1z/6grM0x6bbP7U1ZYQcIeA/UD9xkEhv4j9g573prb3kP+3c8B8xDOc/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j9N1/H0DRboP/rh0a5j9OU/XGzOHtDS4z+OYxg8R7LhP7Fm6/LqJ98/xN8bIgzx2j9zt7zb4sTWPy77uZJmu9I/9dr431vpzT/6ynWv8O7FP94mOTzi17s/Cz0sZPuVqD8QjgLgSVBav8DcwyOCAU2/eJSuacdOiz9baUDTj1KWP9LeqGVPJqE/BDnrVQwflj8SWyFn3xuTP8/yWVm0JJU/1rayH82gi7+uvT3k39q0v9gTfZ+QfsK/M+sz9qxRyb+xeGN9HjjQv/6pWRxrh9O/dySQTmzk1r9vSSq1Z1Tav5YKpB5Uz92/Y0hF6dOo4L/Z37fiwmziv17K9eZVOOS/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G0792GJ+sDf7Iv9YCQsFEM8S/hNWAgHLYvr80vKgQyle1vywCM4++z6e/jth8KXpnhL+eOobehKyaP6ekVVsjsKg/EHOHyNU8sT+DxhUAGe+2P6eLXiIMDL4/jffzD5Tdwj/VwAcw6EXBPwUl8NHWlb4/S9DXAbEKuT+Fw3Y4t2OtP4CQe/qtu3C/YqSGDDh/m7/I9R5Ri4SYv5dSgpaIHZq/nqGilucakL+97Vtx0x6Lv7Q3dXvdl4u/roQ3J+H3cj/6xWCah4WkP1wo3HkbALY/N2554vxBwT8jZx/k1LzHPyM18ve4WM4/ARjm7HaE0j+ULNJvUuPVPx9GKYzua9k/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D/W2Xl5qxPpP2C8CZY3t+Y/bK+I01Za5D8qwjcSoBDiPzQEGnddrt8/xtHMKGEx2z8Y6OqeUvXWP65iKgLg9NI/kZhasgo4zT9qpMWg1+zEP/id1ewMQbk/1qcd6OwfpD/LKwctZAuaPyDbo1P+4Do/fjIcB4dcnj9fMbLddkSiP5VXn1xM5J0/84w/WFkOnD/Iayn+78ucPzOvRIym6Jw/pPJrmTYHg79khurJKn+xv07cwi5Ymr2/3tFOrJzzxL8fTBp6os7Kv/TP4+m93s+/zg5RGitr0r83D3EiIuXUv2lwoQFzXNe/WOQTe+3Q2b968mXtyULcv/XxZiVIst6/XVp1BCPP4r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAADcSQHVmzZxP1prUtrrtIA/MI6QqtMrhz+7ynr9z1aPP849Z5Xlt5E/aCv0JKjrlz8iTAo1gpqdP4ZRUsHfraU/Bsz0nZnVrz8Gx5TVCAO3P/du5F8dR78/0iR67frFwj/wx0bjAfLAP2QZVlXC+L8/F/aa70pUwD9PJ5rgCfyvP0qMmyG14ZQ/RlPw7luilL/RV1mHpvigv1jERvxgS4+/+HK6SJE0j79o1MSpahBtv2wfCxzX6KY/fK4n3uVguT9Hz/y7TxHEPxHXNVelCMw/7wxQV/YK0j/k+XQ+XRzWPws6JmEVNdo/KYsOJ5pS3j/jTdTotjnhP7uQlNtIS+M/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G078ayj1JTXzNv+vjVOQVC8q/C5rR00qZxr/V0J2P/CjDvwhsCmDbeL+/1DiiowKsuL9zuAPKpPCxv02LX13dxaa/ex4TTn/0k79U6XPon9tyPzss0BdEVps/Xi44cfVWoj8imV/mzk2cP6DK677UK5U/WkCLwQdliz+IcGCg4kqdP5WvEisic6s/GX5wTp2krj+Eq0D/gZKmP0v2SZ8VdqY/PnCldKfuqj/awi0tcISyP5+7IlqS17k/JiA3vsnswD/fXawIuBHFP1lBOUZq/ck/xcbpiWM4zz9nggFFH0TSP1bjIbCm8tQ/UGX6e6+k1z+V6IjneFjaP70oJaAXDN0/XFp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j8qMxnUswHpP70X9gnDJec/HFzIm2pL5T8+5xWzEnPjPz/dbEmAneE/LWCg/1mX3z9rAErpBPnbP7QRKUyBYdg/tsZbQBrR1D/RC3Yj+FjRPxqKKjBUE8w/gpsOyeLxxT/rdah3CSPBP/jEH6tpj8I/XqbI/N6dwD+SMkimxQu2PzlTmv5WcZY/C+BkHJtymr+/J7ACSUWpvywF//nOCrS/ktmaS1F4wL9LJB6NIuvGv86g2/6Gcs2/Xwt/DjcK0r8rC8C6r1bVvyVW4XFcati/eopKuFZk279R8gsGrl3ev6a6y1Pdr+C/BIDlajc04r8+x46Md7vjv3PuFBusReW/qfSXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(157,1,145,0.35)", - "width": 0.5 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r/1sUntqO3dv5zwnYkHK9u/r+Kqjadn2L/BN19NS6TVv1shPvv84NK/Ae4ZcZAc0L+ewo8kDpfKv/ALD9S4kMS/kpvMTHjuvL9ajrs2WQCxv6Z1abrJ1Je/jfZ61IfwkT8eTqcvF4FwP6qfxHj/KWs/N9ReOm55oD9uJcJ2ndOlP75LZkK87Zw/sYvSXSgDnD/a8K5lq+uZP6QukIfWZJs/ToMIaDusrT/39Ag2Vqm+P0hWpeb4ksc/ZQ7oAhjxzz+pY45Q4DXUP6TXyTk4jNg/wA8FA7fn3D+wP9os153gP0Q1banIxOI/hwytSsTp5D/WidwFag3nP4WGFoIoMOk/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T/YFtR9FzLlP8WZe+Nkk+M/2oeJgVH24T/zV1MV9FrgP+DjDX3jg90/TSmDeE5Y2j8GXYyeRj/XP1LLemc/g9Q/n7lJFKXT0T+S27vSXlnOP7XwuOXw6cg/RZ5dhd/Qwz/SaWy4+snCPyFzSkFo+78/saGDfkCavz+bjv3k2g2xP+x73qnBg4o//MgudB45mL+Sr7F5V/qdv/QUkkVOzYO/MYpTl/fzj7+ZiI/gv8SRvwRNgPSkQZS/RmAfveG0lr8SIw96kXmYv/pBKeBiJpm/Ku7AoZRKmL/BvTn54mSWv3SqaHzgC5S/tJGP1at5kb80+PLS9oqNv7lR7uXl9Ie/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G078Pa9Hw7PnNv3qVRwO1i8q/KsSUfOQox7+Cmb8IjM/Dv9PoVIuGfMC/3tq5sp9cur9iDyqKHsGzv8jWnXa+waq/y5rzEfianL//aXcbSkdxvw9TECfs3pI/ls8ImPhRoj9Rx4wBh0iRP/DB0PmePYE/6Yf65a49lz+zLkmr1wmTPxZhqQTdHag/eLzyyMOfpj9zM64jf8OkP8uPH8trx6Q/AZ3MooUZsT/upz1NsEm+P0Xxle7Em8Y/an8DB89Wzj/ToOC2sR7TPxEue+UXLtc/M4v2DE4/2z95dKIjzVLfP9kokoL0s+E/CxAqPA+/4z8tNF3RhcrlP7i0Jcgi1uc//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j/EMRojggLpP+kip3tUJec/PQpyC7hJ5T8yl8JstG/jPwDyS27wl+E/Qbbknt+G3z/Lt9zdheTbP4hL4eCXR9g/FRuBowOw1D81LM1cKy7RP3W5xYk1pcs/yKkPSfGFxT/kpVGtIJ/AP+nLslWomsI/mbgsF0aVwD+5YxQHYMexP5LENz84w5k/cn1rgmnqj78DzeoNcsejv0ETOND4La2/5v+EhTtgt7/NiZ7yyxq+v57dyvlc1sG/gUj7d8tcxL8QLEtF66LGv2I5S0GnZ8i/DeJX9MYcyr/ZglbDAcnLv9JbkIDcc82/bNs/LXMez7+sclN8jGTQvwUY901LOtG/VOkvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UukvN+/G07+C3+4tgDXSv20u+jNuTtG/8GVKy31o0L94pPTVjQPPv7kbhWOaMc2/HWxOLbw/y7+6CT33hXTIv5ja2A5Zl8W/f+rWvZOXwr92zk7cgvi+v6AgfHgABri/YLV8i6t+sL9Q+HI96lyfv1h/kxdGZlK/f0oFQY6hlj+iJg3Crt+dP3aAochouaM/BPU4SvD1oz8uYovfU6emP1LAoFl6yZc/nErl+CG7qL9vjB8fmkS/v6sunriSDMm/QmskFp040b+UUii7CObVv4hBO67Hjdq/kasxmjkz37+pCFhOvOnhv7Nl/1aNN+S/TCmUoOyE5r9L42XT79Hov+jxtD2cHuu/AAAAAAAA8L8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7r91cyGq1Tjpv2Fw89rg8+a/E0NIbBCu5L9zYt3acmfivxHirYIlIOC/VuBDBqCy279EQmp50zjXv8e1tuzyxdK/kOFNbuWuzL9V59hjwtnDv44GeghuRLa/t7YUF9MxlL/qd+g+A3GnP6/myDU4gbw/7jiULpppvT/E06w9bfmwP86rGKP+7I8/ATWpxwWZoL+R2ReWvsOmvz2SHgnr+LK/GWLiZkoktb97+j2ljjW1v0YfP5nyqLS/PfiT0ifus79fFbH8uSSzvxznKYnXXbK/93W9v0Sfsb/Xp0XtIgywv9DH0AoYYqu/c1tD8Cyhpr+xRWj/A9ihv9f5Y02EEZq/B1wUMyamoTw=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+cuASuMx/hv1w1MABYWN+/u85wAxyp3L/pl7p7GxDav/5ZQU50nte/0Xfzi8A81b8pmb+X6gXTv2aBORB34NC/Awmm4XOWzb+4ZmIEx3vJv3DYqbXsdsW/XRh+O1SDwb8jMGDGLse6v71memeqJbK/reiXnxf1or/lfv5hTb5xv/ppMjqDqW2/B1RblS+foL9ZrcWKsP6uvy7p4k/iX7a/MpQ/DfUNvb+E5afuL7PBv9MzHPR+z8S/toa1NS7Nx7+X7JVj677Kv7SOoE1hlM2/dQwTAdMf0L9rbt3B2TvRv3yFxezfPtK/8FY09zz+0r8ZnDBN9prTv/UeNYUu29O/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T/ENra5LN/oP5J7SiWf0uc/QqaH5/Ok5j8s0amTsWnlP5X97G4tG+Q/NhURsJHE4j9rjoJzIFzhP/HqmQcx2N8/AgAGGfvq3D+1DIMND/bZP44qeac199Y/OWjIKXTx0z8QHdKJYf7QPw2nSsbySMw/8DE2fY+bxj/Gv7xA1NfAP+Z8nhT1S70/q5a5UwTZxD/ECK6BdxzLPwrsdVIzutA/qHemHNbq0z8h6BfzpiTXP7L7epy7YNo/fTXKAzyj3T+NmfAU2nPgP3p+h1X+GOI/RQWAB/XB4z+Vrx35z3XlP0jV1cUULec/jyH5q2Pt6D9ThKmzDrDqPyWDM8E4eew/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6T+q6jYCHkjlPxveJZS8teM/JJymH+se4j+049PdbILgP9b9PkULvN0/oFsi3Rdb2j8QQvdUeNPWP9U8kPyrBNM/+Zq+7/GOzT/2kPchIkzFPw6y+DGo5rs/hTWogZCKpj9Ql2bM6flxv9op9B9IFIG/x2T1WsGPoD8HfqzIn6uVPwTjyti80Z0/LwF1MRpvnz+f7XgqLxSpP08WJckDOKQ/uLfeZDTdfr8mqiWJNcqyv1cqU+K2r8K/4aa9ilLxy7+dIXaP+erSv+7+YJmZr9e/vkCJcLui279chfvQBLjfvxeMQTuXCeK/Sw4nqXxT5L+lR7hTzabmv2W1sMSj/ui/AAAAAAAA8L8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "YFp1BCPP4r+3Zy73wQrZv1UwlJYgUtW/s6fpzAqc0b/AmMek0tHLv5BLne1AdMS/NasKRPJJur/wYJsHXb+nv5oe/+zxHoE/Lv7UJBW2qz/YImQmvT+4P4rCblCrsbw/ctsFiyTFwD9QxGs4KT/DP4VV1S8RfcA/7orEBKf2uz8kndE2BB6yPwSNTQpuanM//Y3k7RZpob8vjQG+GjGSv9o4GsdFcYO/GrAARJCBhr8GTLtyneePvxwG0ejwfoK/BQJ/g7/dkr+c8KvI+QaevxJ5SqGO36u/2kqIXDIdtL+WseS99FS4v0Vehk/9nrW/4yaBSNCasr9rBIgj8vyuv3c8qezApqi/B1wUMyamoTw=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8L9Bw9oVTCXqv4EEfc3n8ee/3lXR002+5b8gCe64eYrjvxpJyApmVuG/+OGf3GhD3r+zOQ1r5NjZv7z1wOtCd9W/JM1/JW8X0b/tRZvho2fJv+MkG+uckcC/8+bY2Dmtrr8gv5iiJto/P1TrAJbcMmC/00PTj1FImz8kML/9GTGhP/QOFqLqWJ8/xbKPYpZgmT8x7MbD8/2aP4LSBmcp6Yo/zS4D6wu1mj+MQkcaB6SxP6h2LVnj9rw/JlEU9hg1xD+3QoOHRAXKP8tDAAb7AtA/aRvWGMTl0j97Z7hf9ZTVP20c2f9xJ9g/6YDrW2Ks2j9AaWeTeCndP8BzuOrun98/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "B1wUMyamoTxbW3kOe1yCPy8UVqgrzY0/4n3J/8KglD8m2f98T16aP9PlQrTtD6A/Uth9wdbuoj9KAv2Oe9ClPwPJ9T/OXKs/4qWPMZ7hsD/YZMmmcja0P5O6YQ9F07c/W9GR6QKSvD8oq/RolvnBP4SQAMWFksA/WWyi1jowvj868DvvLzmxP4Hh38OKbog/xZ5w741Jmr+OeoLv4wOavxJwaF0Je4u/SOC7u/Yfpz9CWotn+BS6P23kvsq878M/ErdL+fWwyj+ySsOH0bHQP+D1CbOXBdQ/sn+Bcm6W1z+h6+U6EkHbP1MN2qEH7t4/miohAVRN4T+3hM5KoiPjP5BPZ34Q+uQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+nL7GVVrvevzwr3UWiJdy/aB6+96CO2b/vfdhrx/PWvzhyKKqkUtS/BrMhOk6g0b/wAfBQ0ZHNv6xhd9gNVMe/cRqLIgIWwb+JgGQtWAu2vy6+nX5nmaS/sNm5qORcbz8cy+mxwWWKP51DIBJJWYM/7GJMIHlikD9w4zkI+X6jP4L9Vvk8lqU//wxpH+xlpz/F1IR1qQCgP7XvdEdAk5s/I6NRZoQUsz+quSLjHzLAP80BEVOkxsY/Kn0CHxJDzT8439M6mtjRPwzYzn9wL9U/YHkLNW2Y2D/87cxgtwDcP/gTJaNbat8/rIYrz3hq4T+ksIPoRB/jP0kFsh+O1OQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T8MVjmdAUblPyzeunzrquM/0MBOYsUP4j+wGaLzonPgP/K/X6gRr90/w4piKYt62j8MyI+AYGTXPybq+O+3k9Q/GPAEUEHd0T+37RWzZ1fOP9P7h0dr68g/oykZfCLpwz+uvXiy7zrBP77DiLYbZME/VTso5IoXwD9EGHShA86yPyaDUd+L85g/HiLA5cN0lr9294NoZ8uXv8yfZWW10Gy/WnhTU1i+pz8gO6xa4GK3P0IbLIIOT8E/vz7o2lndxj9MTFYOiljMP83kbRnDttA/iRgpNrcc0z/PdDi8D3/VP1BSCeyq4Nc/Zl0h7yNC2j/fX1l4aaLcP7QPeL77At8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j8GwHxwZv/oP5gx+bPA3eY/i0xBf+665D/2Qa8Jn5biP4tesSqccOA/LWAC31CQ3D8nvF+frzbYP3OzY2T9vdM/DILBNBZkzj/0SRqh0FPFPzgLiOf/kbk/xBWd6353pj9uduhbHJuLP0q9IB/k7Y0/ykIqyvqEkD/2F6nR812kP9+nU06UcaY/7qdnsE9Ooz8OpvBLL1OUPw+PC1zoY4s/IlyKTNRtlr8K2OP7REe3v0qyvFQP98S/b/C3q09Lzr99OL3p5J7TvxGbMCrG8te/ZTMBQ9NH3L+P/YoygVDgv7ol9MKvdOK/f/H3vw2N5L+zn2d4+Kvmv02oMRPjzOi/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z9Quc5b+gzNPyCTl+UO28k/rporzOuzxj8BboCUG5jDP1F3OFp3isA/U2cC/Pwhuz+pAXNryna1P6ESoprLIrQ/UFIst+Q0sz+DQaGtNIq2P1qgLd8QFr4/dBSay4NOwj/ye/eta9XAPzU11rzrJME/mT3dkopXwD+Q4B53+CqzPxbN1RZCdpY/mr9nhO/+kb9Ot0Huu+2dvy4g4E9jlHa/sNS8Z/0Ogb+7ZeraLXODv3qpJYF6Aoy/tr9ug1W/lb9fdPqUX2Wiv2ghp7AAZay/sK5/yB8ptb+WfrLSmx68v4M7IOZzlcG/MTroegMYxb9HLPTMRqPIv4/0vB5+aMy/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTz5kZG+UZaVPwHsIJ95vZ0/s30Jk0Xsoj/NyTVLQ/GmP52F/7NP6qo/QLa4H8kMrj/P+xOat2evPyj4648eW7A/TbGf8WjnsD9n+YvFxzqxP6OFBYQstrA/ffK4omJ9rT/O82LKbhKVP4CCR60Ov4A/rS3UT8uukT+5y+2dlT+eP69SsZSu4aU/sCBu5GsaqD8oArkNYpWtP6gnvKvA5b0/sgztm1Duxj8cTaMqrsTOPwttUhPHQ9M/8RQGIRMf1z/fUKP3SvTaPyIsOoAnw94/1bshLYVF4T9bOW8JBCLjP2GwEruE++Q/nGz93U3T5j/GyX3xhKnoPz1GSbkefuo//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D8zZm1cV9XrP+Rkpz4H4+k/voa59Tjw5z9nIFBLxvzlPzrqYQ6HCOQ/zzXvh3cS4j/SghkO8hngP/BAexrYQdw/yOmX+CVI2D9atBNtNUbUP1Q7eQ7TN9A/q+Y7dX1qyD/XvcX3vcvCP5KGwNigo8E/q2ojSdH4vj8bNvOsgcOzPzRr4oMxZI8/b7A+cMgPn79kAl/FmjuZv8h2yeQRdEy/TOL8gLN8kD+Y37UaVSSiP0Ac/a9ndqw/77jB40Klsz/PiAhC7Si5P3PanATK0r4/WSKSDFY4wj/gTNHQRS3FP7Rx0rD2J8g/wdH75+4gyz8L8cGXcBfOP2zBDqfphNA/T+kvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D9gEiaLihPpPyyUvF67tuY/zZzp0d1e5D9/r0lEngviP9bsmVPudd8/HxJz9EsA2z9Q0SBWEbfWP1DZiD2AntI/eXMCtGZqzT/vSG8TjFXFP3e9a/K0Brg/Sm3E+jKAmz+AOpcLpo5vvwgv7ufVD0M/4FLPCSjUbz+yl4fixf6lPyYvca0Jsak/Vh3n26SGnj/PRJe+li+bPzAZXvCHuo0/gArIFtCZkr+TOXdnE/K0v8QU/u5FKMO/zsh3BSKzzL8bzMQmOR3Tv6TfGr+inNe/ExvMksLz27/BIJto5Qngv6RDUowrKeK/lsSQFpVO5L9Ignh1PnPmv56ZJjXPmui/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAACpFZnFoiVrP4bq169sboA/i7Dsf0+Gij/wuT1mv9SRP4c3Lo2y95Y/M7PmTyvamz+gr1n6O46gP74/aggR/ac/OOxwpKr4sD+UMsixN6K1P7mg2H0947s/N9a2oDJswT8IshpYrBLDP0L2+phKssM//lD3PWLIvj+YJ95efvq0P2AlZZ+LDF8/IB2wkZExgL/AStaDBnmAv6z94LW3Zo+/deYEQhiQiL9N2ebm0WWLvz0U9iR46YW/YRF56Jb8h7/NBtKDV46fv97YnSpbxKi/lMBWh1tqs78h+aVIRR66v+e6Gu0P3sC/CTiAcJKcxL/ceHCJOEvIv6VQTZz1/cu/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AFVEEw5v7r9Fd3welybpv72SDosNIee/EZGn03Ma5b9+M16ocxLjv7pueraZCOG/DgNgOon43b/ONF2/p9fZv0Ltl0ajntW/m2YHb9pT0b9dwFSdTuHJvzYwOTKz48C/Ig6EGi0Fr7+8RdA7Oat9PyLLMgCZEXw/wLpUEDK3kj+zfb4ctFacP9gzT/6S8KU/nwwxOa+Moz9Eg7zgqIalP97Q8aYDBJk/73uTLhxlpz/DWIdunWyzPx0yWUP6Vbo/lTrKs8JrwD+c6e1v+3LDP0hA1CXrbsY/xCzjzmWmyD+600ORd83KP6NhEsGt6cw/YT2Y8xX+zj/GKgSTRobQP37kVo49i9E/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "TekvN+/G079/of5HNo7Mv3puQJgTeci/v1qndIljxL9J8B0Js03Av26OPFUzb7i/if1oVHdAsL/wZolrbSKgvwD/vA6xwFS/4JriapXmnD9qVqsF0i2tPy8u/KY8gLU/5tODE/cpvD8uCZW0D6fBP4AbsVnKfME/goYV6P7YvD+xeazXTVqyP4lpzwIEfoA/ZrIcY66Vk7+GGNPh2AGZv34E+i8XE4q/ofNnWMNWqr++puuWSUi7v8L9i9M6gsW/qi87+E+9zb8nwN4wRQ/Tv2rhXUahQde/quaCID+X279yIRoZJ+3fvz8ZMyk0IuK/AejtVERO5L9HO6TzC3rmv5OVzVDmpOi/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7r+1j6nJ2WnovwT1ysZ/Mea/XSLiI3D347/Bm0mTULvhv7MzvxL2+N6/pJfi6yx02r+IdI13I+TVv9s4OhLrZNG/RmbZ0Krpyb9UaspXlSXBv+JoFctMZrG/yAQ65moChb+Id0izKCGBP/St0SKfVIE/paxDoPnomT/S3TYZi6+XP0KrxfGoUKk/9fSLiZxzpz8rUexYycujPxAfX7jSh6Y/HNamvYi4qT+oGVEw+2yqP/wiCVT7MKk/WzHXgTjwpz8yt2VxvbmmP7W+Emok7aU/4k10rKM6pD/U17tvE7ehP7IGWupwY54/fs5Q/RIPmj9zaaZUdmSUP5mNDz1XQZA/CoqeTDl5qrw=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "UekvN+/G0z8Mq+hUO5bUPy3txp2IhNQ/IcEBM4Bs1D8dMUst2EvUP2pRleHsHtQ/OGFIX8rf0z/Yy65YuIPTPwBgN0VwnNI/7gNijTNF0T+D3/2a9krPP8l1U74fTcs/2Bg0a+3wxj9INvBQm93CP6STYfHSEsI/y905SSEauz+t1bDIXsazPw6q7gHGsYk/+owLme2co781RsMo+najv5POqhgvx7O/6KUf7jWiwL+pHCISxDbIv5KjYQReIdC/5oKcufg61L8CBEn6x1XYvxcO/Frwdty/yvwpz/pI4L/Z238NU17ivwJkgsEgeuS/hCvVzLKG5r/7DPJXEKnovz6yZBhwvOq/AAAAAAAA8L8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6T9J6X8WzzTlPxyNQwPHn+M/kXI7B3MH4j8UjCXhCmrgP2v5sCYwit0/JQsHu1op2j/nc4sx+qTWP+vBMPYK4dI/AOVA/gx8zT8D40t1VEnFP5ZywV6QNLo/EtpNwJy4oj9oq/bPMeRnv14mNH3ASIm/ZrnfkZG+oj9EOFutWjakP/xIsV88YKA/hRdvq+eOpT9C9D6RyqeoP039DVkkg40/KJ1RS+5Oir80BmejbAu1v4AM5sgOAMO/sfcdhYVezL/TTRKPmvjSv1bED2Embde/2dNO1wKT279mjzNW++Tfv5X/yOx2JOK/uF7oq+xR5L+2kFVa9XTmv0ugIWyxmui/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "YFp1BCPP4r93GRZc5AnZv8rGbJX/UdW/VZyWIfmc0b89FqLbUdbLv4KRKBP7e8S/suc22ylhur824o/RGf6nv1zCzqE/BoA/womPAKLIqz/eUr8Cgdq3P67iFiY1vro/1AaTUfpxwT8P44wt+Y7DP1UrnJIBvcE/9PmLdcWvvD9IoactpFauPyA1Mcgf7Ui/cZSS+SaUpL/6ZQahvqWCv2A5i6ZLQU0/qOsf/Ow1br84NWvnDzxwvw7K3Lo5XHS/rmiB+10skL+2Rf6Je4Gav/aIJWmvVau/oK0uYN2NtL81+95wm4a5vw1ZkmPChcC/TfGR19ZBxL9lu5D5tNjHv22vkaVrm8u/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j8gG/7OkzHsPzy/QQ93xuo/bLiHcGxY6T++ijEaK9/nP6pVTplPNeY/ZlfKNcEv5D9RQXmtRxniP+hAvoR+yN8/bQKJnWUQ2z/W2VAjXFbWP0E2INlnzNE/UFQ02/7cyj8QSiimsB/DP/uum0eIQbU/+BcNGqhnoT/PflOWizajPyvllNpIaaY/Lqf9+kLOqz/T3mtVlqq8P4+REZ6P68Y/8pFL3KtSzz+zB3ITQybUP88r2f+siNg/UX0H9u7N3D+CvA0apmTgP3KeMobMIuI/E8HUw66z4z9wZAl3byblP/WBFRZPVuY/n4fPrrE45z8HOGSnSQ/oP7MQEOXl4eg/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G07+F5U3cWtXGv05VR8xXRb6/ZSKWUjezrb+gy4onsaNePwBL32v2Na0/f/Sx7nDNuT/5kyC07+PBP3BexOjNM8Y/QJdXHz4ayT/tD70rOgLKP0NKvrAq2Mg/8Gw9k23Wxj+W/k7ASsnEPwLZyR17mMI/ncjqbnLNvz+vVLVZXP6zP6o8hBbwOHU/t5LARQIKnL8r2Y1rLU+qv0Di5FH9g7C/SdjRiX5Js7+25TY+2P2zv3A+r3vKrrK/ZCglozIopb9ADGrVIwokvyZxA7qqNKw/hDPhcDggvT8X7LqeLE3GP8nptbzqqc4/z2LrejXd0z8drEop5WXYP73Zce1g8Nw/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z/yV6zZi07RP2tSx6T5SdA/iEaeU+OXzj92vPJRrKTMP6WxP/xmuMo/FzhitFTYyD+XvwLjPMHGP+3PKyjHjsQ/3qyp3Qstwj/yjdPC36S+P736a4YXt7Y/eoJX5pTyqz8yrQbdb1aTP0K4oOJLH4I/CJm6dV82iT9ivYpqmwKoP3hKQ0ldG50/si8N9sVIoT8Mw/D7fLipP8UXkIPAr64/HFSKT1hGuj8zwKCSUzrDPy7BwEuCbMk/zLqPg42rzz9IzpU9Y/3SP9z44VivLNY//FFgALB52T+PcT8fDejcP5cIm8UtLeA/1nxTxbfn4T8ymffgYKTjP/NoP4zZY+U/p/SXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j/aAoKKI5DpP8Rk8dcbo+c/vy9HBoK45T9ICnvJ/dDjP1SMpJjM7eE/JK7sT3oS4D8mz0rFO4DcP3hADYvR7tg/Jf2lwFZ71T8BuRSx3ELSP2SueEjOiM4/US9elwXoxz+nxP77QHPDP6+Jxugz3ME/DKEFx8YJvD9WWKgLPHmxPxBx9VLRKYE/5NWuVfH5mL8jMAw9B+OnvwVtGXHsnra/kdts2EDlwL9ZFDmpD57Fv16twzTQI8q/8RgFauqozr8R0QoRFKHRv/MP2X3p89O/vUC40N4h1r8Mw2UgVDHYv/MChCe7RNq/NXLi3VBb3L+MFM6ZCHbevyFfT197S+C/YFp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G079GyrNGLb/Uvz6rN1Zrt9W/Noy7Zamv1r8vbT9156fXvydOw4QloNi/Hy9HlGOY2b8XEMujoZDavxDxTrPfiNu/CNLSwh2B3L8Bs1bSW3ndv/qT2uGZcd6/8nRe8ddp37/1KnEACzHgv3EbMwgqreC/7Qv1D0kp4b9p/LYXaKXhv+XseB+HIeK/Yt06J6ad4r/ezfwuxRnjv1q+vjbkleO/1q6APgMS5L9Sn0JGIo7kv86PBE5BCuW/SoDGVWCG5b/GcIhdfwLmv0NhSmWefua/v1EMbb365r87Qs503Hbnv7cykHz78ue/MyNShBpv6L+vExSMOevovysE1pNYZ+m/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j+mRRcq4BTuP0w26kCyuu0/8ia9V4Rg7T+XF5BuVgbtPz0IY4UorOw/4/g1nPpR7D+J6QizzPfrPy7a28menes/1Mqu4HBD6z96u4H3QunqPyCsVA4Vj+o/xpwnJec06j9sjfo7udrpPxJ+zVKLgOk/uG6gaV0m6T9dX3OAL8zoPwNQRpcBcug/qUAZrtMX6D9PMezEpb3nP/Qhv9t3Y+c/mhKS8kkJ5z9AA2UJHK/mP+bzNyDuVOY/i+QKN8D65T8x1d1NkqDlP9fFsGRkRuU/fbaDezbs5D8ip1aSCJLkP8iXKanaN+Q/boj8v6zd4z8Uec/WfoPjP7lpou1QKeM/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G078ayj1JTXzNv+vjVOQVC8q/C5rR00qZxr/V0J2P/CjDvwhsCmDbeL+/1DiiowKsuL9zuAPKpPCxv02LX13dxaa/ex4TTn/0k79U6XPon9tyPzss0BdEVps/Xi44cfVWoj8imV/mzk2cP6DK677UK5U/WkCLwQdliz+IcGCg4kqdP5WvEisic6s/GX5wTp2krj+Eq0D/gZKmP0v2SZ8VdqY/PnCldKfuqj/awi0tcISyP5+7IlqS17k/JiA3vsnswD/fXawIuBHFP1lBOUZq/ck/xcbpiWM4zz9nggFFH0TSP1bjIbCm8tQ/UGX6e6+k1z+V6IjneFjaP70oJaAXDN0/XFp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j8qMxnUswHpP70X9gnDJec/HFzIm2pL5T8+5xWzEnPjPz/dbEmAneE/LWCg/1mX3z9rAErpBPnbP7QRKUyBYdg/tsZbQBrR1D/RC3Yj+FjRPxqKKjBUE8w/gpsOyeLxxT/rdah3CSPBP/jEH6tpj8I/XqbI/N6dwD+SMkimxQu2PzlTmv5WcZY/C+BkHJtymr+/J7ACSUWpvywF//nOCrS/ktmaS1F4wL9LJB6NIuvGv86g2/6Gcs2/Xwt/DjcK0r8rC8C6r1bVvyVW4XFcati/eopKuFZk279R8gsGrl3ev6a6y1Pdr+C/BIDlajc04r8+x46Md7vjv3PuFBusReW/qfSXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "Xlp1BCPP4j+vmcfgpKneP0TZW8N/39s/GOQ0X50V2T9ySRCzOUzWP8M6nDVwg9M/0cnHYkW70D/0HT2Bxt3LP2srIuaxUsY/X/kxhB4hwT/e6EKu4U24Py5umUL9l64/rqX1TCcCnT/cvGvHSgJXv1WxwZf32X4/GbiR3ilbmD9kGAKnmWOeP59uLLhJcKE/XqqXp6+goz/jnqcTzFedP0gQ/sqpZZa/BORO9k8PtL9Cy6E9HunAvxBiUhc4Oce/5KQglJhAzb9Ll+6b8XzRv7tq4hiCRtS/BlFmmVt61r9ZUVe8pJ7YvxTtddczu9q/9SsDuNvQ3L8c8iQ2K+Lev+2hxQwYeOC/X1p1BCPP4r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T9fANiO5vXlPydpVxt7bOQ/Xzt+uizk4j+4BuM/sl3hP74cK4PLtN8/C7BK24C53D+ylm3mbMjZP9qVwUDi4tY/roXkXan+0z/uUXGm5gTRP2U/H6QI0ss/n95Y/Q/TxT84Pnl6htfBP+kxcA5+ecA/snnOIRKJvj/w886j1UKuPyZzpKyR3JM/zr1hioAunr+27SqNuoyiv68vt0J2Vqe/eZxHapkJs78xaxhce469vyXgpy8hysS/SQi/JS8Cy7/vI3glSbXQv7qaex7f7dO/693Y+xOT178t6QjcNT7bvzSC3B1T6t6/2pfVcThL4b/Xok3eKCHjv2aw3Rr39uS/p/SXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XFp1BCPP4j9AUGfMBMffP6gIiOVcy90/knCyvmPM2z+xvaQgCsjZP3wDxzuXu9c/nMyx83ei1T+sFc0U9nPTP5MJXkSZGdE/AyvOlTynzD8E5tnO487EP5zP0g5+mrk/ozK1cNKCpT8jyWRpo7CKPzx1L7vcipk/oFAzbJDZUz+nE4FXdi+oP8DHMwQTyZY/otvJAaHCrj+HRl1TRXKgP32R/2dybaA/nMEnhekYYL/MtGhqI82wv16KAvdBgsC/+tzpfwe7x7+MtbbtwwfPv8aZcjbB6dK/H3+R/NNj1r/S6mbDWfXZv8pq+urbkt2/KNfEWsma4L8nwK0Fg2ziv1FUIxZiNuS/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qfSXm3fj6b/81AHLB3zhv1kmeXNNGN6/KKLdW/A62b/VFIBFsWDUv8Ia+OwmFc+/CViu01h0xb+QDJRbOs+3vyxZvPRgtZO/PC9HI+bVqT8amOMOPmm4PxeDVZVJ870/sPqJOZYewj/qQRkThPTCP9X4vf0rlcA/UhYmEkYLwD/aleEtlfquPyjd/eXowpg/enGaxqgDjb/UKKOnJ9akv1mxqiCkcaG/kVQ3K3r3nr+rcxRqVruavyQlZueojJa/BrcoEFsDdL+mrepyGQ6cP3DUfNPq2rI/tw3IYtq1vz+dVdXKR5zGP2N5S/X0js0/70EHQ9lQ0j/NkqnRyeXVP24S9/AuXdk/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j9bYG8L8LHrP3hk8AGDv+k/NcBdqSXO5z931RspDevlPxKeoLBDCuQ/z1mQ+2Ar4j9qC6ezC1DgPyZ1Faa+79w/1nhbQwqA2T8STSjTQLTVP/hNyGy4x9E/bdXtKWShyz9D+lGsmXXDP+onewmijbY/jK6cNIB8oj+vB8KW4wmkP5rGAlnQK6U/QnPjPyIFqj9+4hHh45a8PwpJwXQeWcY/mV8ddEIuzz9AeoNUrI7TP20fmopVp9c/T6aLZ6aX2z+ZP7Q/hxrfP8F1VQyRRuE/53UCLeUA4z8xF4vQe73kP3Gdxv3FeuY/wr1EGCs86D9SmZpodgXqP/ToJy3e0Os//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z+iVQaIYNTTP+8G0vpif9M/a/gIW9op0z/ZzYMQ21bSP/z4CxAXZdE/Ogu32YBl0D/i0wQNAs7OP8F1s0K09cw/f0YtxArXyj9IQT6zkszJPw5e+OEqp8g/BFMf6oboxj8IDt4KDY7EP6KZWSlVuMI/l+QAQHeovj+BTjyunvuyP7AzoPqi84k/VU3GwH/voL8wdlcx7yisv6jt1mUfQrG/cM5uTG1dtL+SA4BZ98i2v000scX407a/iI2udxmdtb824TwPyiS5vzho4dzC6b+/a1XFLyV1w79TzQRrVfbGvwWlLX8Nc8q/u9SNWM3Dzb9vobN1lGDQv4ccPA934dG/VOkvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UukvN+/G07/A45WkfzvSv6hLDT+sZtG/pzvrlj+Q0L812rJCu2zPvyoJxmSZrc2/VSEQVXXcy78uAwyBM+zJv08qOtLvbMe/bLir7IARxL+cMJpV4GjAv15MPe9YWri/o53LnR5Eq7/gCqNf0Xxgv5S+M4AIrYc/mFMYdIVSoz8xWAxr32uiPyePTbyw8KY/rHNF7bRBoD/W8RuPx0afP2dag3bJJpk/XsTk2raocb9G4ltg+x+fv7ZAe9mPE62/QXjkRqIttb+8Wypwo7y7v0G72fXe3cC/khltPXWTw7/JnWAGMUjGv9wTtpacF8m/KemFjZivy7+imuy9eVzOv4uuWj+db9C/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7r+NnTAyN2bnv28n/ifQ8+S/8WxJn++A4r+IezVTHA7gv5j4nfIhN9u/JUfuhZdS1r8RnDr5SW/Rv3uC/15DMsm/1JlKL0Klv7+AwOtvkBCqv0lr6t73r5Q/9/mjcotXtT8vK/ubXX3BPw2A+kMXusE/3jf0pyo4vD+BbLmvRzm0PwAPYgBDpj4/wIg30UqSor8IXYyry5Cqv2mbpYExs7K/9ZfScH8yob+iG5ZHp7GbP5Cjafup27c/nPsDBFG3xD+MM4HvJKXNPxecD4MKWtM/cXyQfDLy1z+aggjwBJzcP9lNX5tjkuA/TZmJ1jrw4j8sPHiyrEnlP4ApX0pinOc/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AFVEEw5v7r82r5/BrBjrv4FMMp6NHum/bplb7SQk57/2G/hEAinlvwlP76Z0MeO/qADE+9c54b+WHbR2SX7ev7wCfePWgdq/ohly62N21r9FDNoRGFbSv+I+UgmIF8y/AxBgC5kTw7/EGG64leGyv8AQYvtsFUm/MpfLkPIPmz/G6gd2XyOiPwyVXO3MV6I/EleSNDgGoD9XVxnq0Iagv0iW2mJbira/uAFKL44rwb/KIs5BrUbFv7mK/5XSVsm/ikyeY6U6zb8Z1RwlZpTQvwr5PPnXotK/l6+Zzpe/1L9JKKzmEezWv1CGLxKrJ9m/xfbPv+e+27/564Zbj1nev5zOsvINe+C/XVp1BCPP4r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "TekvN+/G07+1vZ5tAh3Qv83FmMBwt8u/2IRjVXY1x7+efzWWt7TCv/8byZLUK7y/9a+A1BXksr8Vv3cEmmmjvw1bivPwBWO/tFzMm3GioD+6oWeMcfmwP0oNDAyoxLg/kiLMhq5Zvz+GEo+iNWLBP+WZ+9hUmME/aMiViM+Cvj9PaokMUtKvP0GS9bfX0IY/UE06+e0knL9hEXm2cf2ev4cQLVVwsHa/KCVG1eiRpT8TVkYjWZy6P93vKvsVcMU/W/GdHBCmzT+KzcCVXeXSP5blRYIv69Y/Kb9JC+ju2j9/qpqhUuzeP+kWaY6DcOE/jgnGO1tO4z95p0IfMSzlP1imszTaCec/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UukvN+/G07+yHPi6DvzRv4QkCMDsHNG/gzWbQ6440L/o0GDZVp7Ov3DTTXdLv8y/BaroKDbQyr9yB/LJ2rXIv+sx2U740MW/Ag6pt4TDwr9ue7jGxfS+v2kgE9/dz7e/aHnG8kHArb+ihMYfz1qCvx5YLcZuSGc/sFAFG3vRmz9T10sz5g2hP3NmwPVrkaA/vpXoF2PSoT+cFmfEWiudP3z5CoZdKGk/UjzfuKPGdr/Mk6dlGsCdP+WjmNi5KbM/q1gGcEYSwD+k1PdKmdnGP66/1FyIuM0/IAyb3Bqn0j/kw94XocHWP27fVUTs9No/dhmtRbQ43z9imvG5lcXhPx6sqkmD8eM/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7r8L9NAsMyLovzRtFZpFuOW/rljMfuFO478e6XPCiOXgv7HBfVZa+Ny/B5b9YP0l2L/B/WbrWlnTv8vZlSCXTM2/1LS3xVDTw7+qB67czbe0vzDN8ZWoeHq/3aY/lDVqsT9mUw8J/D7BP+LLbk82pME/hC50IfAFvD/wUlJbEO+uP7C+v7OX0oI/3/vRE1Ufob+ot52OJbCiv3G1GDYNbpe/GlJbDONKiD/4mk8qWc2yP4nCeV3aAsE/MfVHhf5byD9AXwxwOFvPP2Z1XvGj3tI/uaD+Pp1S1T8hKzVtcJPXPygP0TmOzNk/jjzKbAsE3D/sAybvMz7eP0D35gu1POA/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "X1p1BCPP4r+6abQLPuDgvzs6ddK5Y9+/r/2V9qsF3b+0nmaJ+aXav7MlM4hANNi/Ii3oe3e01b+G1V6vMDPTvwgv6yT9rdC/VyaSmA9PzL8Xdk1+uz/HvxHFAlE4OsK/wKg8APeOur9Q6l0qEOmwv2rDbnuNzKC/slCMsQ6Ihz+kep8nmr+dP+Dj9T6KoqM/qwfA+++koT/atvkYORaiP4bTn81nNZ4/1RY44egSpz81yIlzys6wP27+A/iPR7Y/48xr8pX+uz8h2GgBw+HAPwfq3FoH08M/manOdbbExj/z6WhpUbbJP/0Ymp3YdMw/Rmp5lSvuzj/NM9uhgbLQPwEpG7ER7dE/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "p/SXm3fj6b+rSvPqE53mv5CeuLnJvOS/kFY1a8Db4r/TAVmG1fngv4YmBD7VOd6/BykG3TCH2r9FO+eZDdHWv6IwgK7+FtO/o032mTe0zr/ghoXGzDHHv3gkFHnGOb+/wGr5ozrNr784SUYsv95hv+XPAYHPE64/qSX/ezh+uz+4IpqvOKqwP4mNAdLRZ5I/YJfNWLP5kL+jngbXJAOVv2/Wa4WskKO/ItOR5spFu7/954ebKT/Gv8JNkjlSzM6/2M95puui079pRheqEtzXv5K8+IdsEdy/ii+Do7oh4L+6pNqE1Djiv4Ko+q1/U+S/uOd7VEdz5r8repvWL5Lov+xBXWxlsOq/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "Xlp1BCPP4j8MBzpPp6bePzlMGV6J69s/KXNi+bQr2T8BEMiEo3PWPzGNqRfeyNM/oj0IOV0S0T97Abvk+IHNP7noOgolrMg/Cincg1ZcxD/OF0cR+QbAP6oN282tebY/EKXxMJFIrT/ccDsFTN6UPzoEQo3kJI4/O55jyUrsoj8JaJ4HCseaP6ZWW8z8ZKA/Toym7ZYAoz9TAIpbQtqlP96W9I62FLA/CfAUvrezuj8Ty9YJk7DCP5RdBzpousc/izy2HWD6yz8749DagxrQP6Ec19CiN9I/2HRsLZFh1D9sP4Gv4J7WP5JM+fVn39g/YOtMTsMg2z/f6E2Qr2XdP3K8cBOesd8/XFp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T8agoO1z53mP5H8xP+OOOU/aX+GqC/Q4z97DKsfYmjiP3wD2iAJ/OA/f0Thh4Yt3z9QvTE0LRHcPzE8EMKo79g/gxoA5i6u1T9SZSq7PmbSP6972AMZas4/5jmGlH9LyD9/MaAfulfCP3y6cTRU0sA/kIYaETvevj+/5orgQ2SwPzbdlVTOJZM/xTcJgia6or+8i1w5zcGrv+Da9NWaZri/CPNiwSjhwr9fZjqJ5TPJvziNA/xHTc+/RmbtWibW0r/BvC4hDhfWvyeI4+97Vdm/78mXUNmB3L91T5A3OJ/fv/XX1BhkX+G/yCA7Ypzu4r9D+YAsn3zkv3TfE+w4COa/qfSXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+K9UD60rnfv3nwqqSML92/45aG2Sym2r/x/DReYBzYv+keSJlgkNW/DBIVyh3+0r8hJOrLH0rQv6d5vxsGoMq/CICdb7eVxL93LkH0VxW9v25eXHLPVrG/+ANKoNw1l79wHnMDfgRxP7CbSEBxsFw/0mntfGxJlj+Gg00B7hCgP3zBqjflAKE/hqLN/abXoD8QFdgdZsOgP+pNoBN6waw/FPNDx72/uD+QYj9sEBXCP2fwiN0rucc/XImJC8s9zT+liWEGGVLRPyWqDc2S+dM/Uv6Ug7l+1j8NXOhWD7nYP6Z0KpO659o/WEhh6/4O3T8Cdpi5hjHfPyc+QxIBqeA/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T8iRP3CSt3lP1yAvk1uRuQ/JFLURmGv4j/OVTfK0xfhP7DplT7x/94/Zd9dB03Q2z+2q5DJoK7YP9g0TdpU0dU/qwZ7lyYK0z+KyilsGU3QP27txCNJKcs/rLL8slXoxT9SCMp7benBPwCA7cJ6H8E/7gbYuTwdvj8s+uv0yhSxPyaNxuysoow/o+bxNvX5lL/yyfWkfROMv0wjlXlZQac/l5N7lZIruT/kywxXQhHDP40nQlKWjck/QEEpAX0H0D9aDjYjHkrTP98ZkV48j9Y/OuHQ/cLp2T9sA2YZ4m/dPxTOHYiTeuA/KDTz5Io84j+3OxzCgv3jP4ToncNVveU/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j/OXb2VOmbqP7HzuAIoWOg/GL8VY1NJ5j/w6nIKmznkP+Ndy27oKOI/LKykI1IP4D+rcrB3QtzbP/JoOeA3mNc/fNoIeOpL0z8z8NEzq9bNP/KZ/b3u3sQ/KLMS7ywPtz+Wqw6x7piVP/F5WpfhSYc/wpxBbckEkD9K4Pv3PDqgP+UfeJgwYKY/8WLRg++YqD8jOtrADOSpP4rmukBqPLE/EyixlBzqsj9fQzPNio+yP00O1wNtm7E/jPFyl99DsD9aLwWatp+tP8mxEbOuwqo/2TW6qTrXpz9kbQndmIikPwaCjItrGqE/IdVAzfBZmz8pJdP3UoKUP1krDN5QXYs/B1wUMyamkTw=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z82QhevK7HQPyw34Yx7Dc8/KmX/Yva7zD+wfYJhU2/KPwCxSbCMKsg/wA8TVLu0xj9ajLf4Ou3FP27VIW9RScU/nS0k4xTcxD/lVccp8p/EP0UtyWn/vMQ/A2s9EbXBxD8p/ZBpXbnCP55QtCSj1ME/5LVFOTB+vT/vdXbV8bCzP45OjhqDZYs/9MvM6S8qnb+BU5wbh8mWv2j5h/wxmaQ/BxekA9E3vD84Aw9qMx7HPz2QjL8RANA/swGr3dJn1D+UxdznvcjYP5lV/epJJd0/A5NcWn2+4D/+OEVQ9ufiP0e4stu5D+U/CRgoCT425z9hUwxusFvpP64tpDg4gOs/AAAAAAAA8D8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7r8MCQsHW8fov2w2iaP1mua/017v8phu5L/fpeHdRkLivx+LkGMMFuC/o733wT/T27/4vFWO/HnXv3Kv18ePLdO/h/kPrGrIzb/N3IHdfTvFvwWcTCbodbm/GhNz61+pob+wh4EFC2cyPx/ed1narW4/UpDPCmEKkj90LwhaqTGgP+cVwLAKXJw//smH9hHRlT/KjXV3fU2eP1St75rsQZA/lihxnoeWqD+/PEVl7gK4P+zG+ZZWqME/1uonEGIyxz9mqhPkq63MP6ZnMgW/IdE/h/L9/h/A0z/brchtzkTWP8d5rOBEudg/DW5drsgk2z9c5nHYxIvdP+GzRTGK8t8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "UekvN+/G0z+/BrEd6HHSP+a987XyztE/7olrezQq0T/V7uUcJ4PQPxXRu4Dzsc8/vEiD5ilTzj8uRAIbt+PMP7KkwWMNDMs/MthZW/ktyT+FMVutQUPHP0C49RAfScU/yQ36uhpUwz94z8ZO41vCP7z9pR1S5b8/FsAvfrUUvD+uz9oXGLGuP66Ay+BY2ZA/1AcFFm48nL9wOJLqUAuXv2iR6sS2+Xa/nnkg9cHwqz8EeNpkqrK7P0aYpyRwmcQ/1WR/sMVMyz+WeQUjWv3QP/DSHO/hTNQ/IJMLJ//O1z9n8BLkv2zbP0BQJJ1sEd8/sSHLUPJb4T/WM8rxxC/jP8n5dliZBOU/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XFp1BCPP4j8lZcZhVJLgPys5HM8cOd8/CEjcUpBE3T8kLsiJuETbP/wlST+yNdk/Zaa6zBoR1z9rrbkKxMzUP6lTGTsJVNI/aNLSOwRDzj9Q/dxKTh7HPwdJdL2bXr4/7FvX4OG6qj+4aSj2O32GPyR+GfwLrIU/NoYnElILjz9Vrj6tbaShPy+Ils6zMKE/H5ESu1Yooz8E+33OdiGjP6zhJ8O+PKA/rj2XpXeLhL8mDNTZE0Gzv3nfIMbfGMO/wfNKm3Q8zL+EQMWLnc/Sv6RwrwM3Xte/sPuIpieB278rp1GiBszfv+0ybEwmHeK/0pRTMzA65L+T1yojCn3mv0J/WyQGpei/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qfSXm3fj6b/t+d6QPwTjvwcbTEbKw+C/o+2hiYoG3b8KMHs7MYXYvxuxhcLhA9S/EqtlAwIHz79uyYB3LwzGv/cvuPoWQLq/qcv7AreapL/aCwaJS6eTPyWKW1pf6rI/9twL5xxEvz9ExdN426zBP7GOyUDdI8A/cxUt4E6IvT865Y/Z4DC0P+QVyszrp5I/IvJbHN5am79Y0+0GwxyTv0wZKXOzcX+/NeIKUtOSgb+aZbZOerNxv4ndivUFDI2/RykI8jwElr8oUPxhy1Wev8zzmbXw0Ki/RqaLEkRks78qN1442gO6v2K56lQJtMC/qsHBVCtmxL+vq3IboZDIv9toCtC+Tcy/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j/y4kjmDbToP79Jq2wNi+Y/77xKZWBi5D920GxO8jniP0pInYq1EOA/v+jCQuHJ2z8ctFjcDWbXPxbpK1w21NI/XQzIlOaMzT9d+/ZyfgPGP2ksl00o77o/fce/f3vWpz8aK3IjA3B/P6T3iCsZ4FU/Qq7ds43xjz/Z/0KtDk6XP2qfVyXcIao/W+JzD7AGpz+6n3PsitejP7InQSyS858/Cqj3v4ZNiL8ILx/i6kC0vy7Ok5uv/sK/PWQPAzV+y7+dMbKMtgPSv+geLwYaRta/3t3+maGS2r/sCCwjq+bev1N/TN3MneG/m8rhddbH47/HS47VIfHlv2SMTQQgGei//1REEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z9QRnIfLk7LP8YQIWdj38c/7sQxeR5wxD+97R/yDwfBPwLuPL9sVLs/nJsCZbTGtD/KwwPvpiWtP+Vsgo9dZKc/owZWearTrj853q1r+Rm3P8ZA/AplBL4/wiPTHzilwD+yrbNnj/K/PzFUjBba3MI/lll8yNh3uj9+4htLvl6wP4eVP7DTHIw/RgYZ4tydjr9bo4Y43yChv+KSzIMV2Hq/q/BBI1hAgb8UQgWffVhqv5BuOoVoFkK/wB0LGHUymT/OJl7uxP2qPyizN7Rh0rQ/ua4ghBWruz/jX3DlPyHBP1IHE03CXcQ/xHykiDWUxz9V3jZMTcnKP5oOKuD+/80/UekvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "Xlp1BCPP4j+vmcfgpKneP0TZW8N/39s/GOQ0X50V2T9ySRCzOUzWP8M6nDVwg9M/0cnHYkW70D/0HT2Bxt3LP2srIuaxUsY/X/kxhB4hwT/e6EKu4U24Py5umUL9l64/rqX1TCcCnT/cvGvHSgJXv1WxwZf32X4/GbiR3ilbmD9kGAKnmWOeP59uLLhJcKE/XqqXp6+goz/jnqcTzFedP0gQ/sqpZZa/BORO9k8PtL9Cy6E9HunAvxBiUhc4Oce/5KQglJhAzb9Ll+6b8XzRv7tq4hiCRtS/BlFmmVt61r9ZUVe8pJ7YvxTtddczu9q/9SsDuNvQ3L8c8iQ2K+Lev+2hxQwYeOC/X1p1BCPP4r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T9fANiO5vXlPydpVxt7bOQ/Xzt+uizk4j+4BuM/sl3hP74cK4PLtN8/C7BK24C53D+ylm3mbMjZP9qVwUDi4tY/roXkXan+0z/uUXGm5gTRP2U/H6QI0ss/n95Y/Q/TxT84Pnl6htfBP+kxcA5+ecA/snnOIRKJvj/w886j1UKuPyZzpKyR3JM/zr1hioAunr+27SqNuoyiv68vt0J2Vqe/eZxHapkJs78xaxhce469vyXgpy8hysS/SQi/JS8Cy7/vI3glSbXQv7qaex7f7dO/693Y+xOT178t6QjcNT7bvzSC3B1T6t6/2pfVcThL4b/Xok3eKCHjv2aw3Rr39uS/p/SXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "CoqeTDl5qry5FvPc8fCiv15qhqKG766/Q+d99KORs79WeaINlwe2v/cRibpP3Le/CGRdMc0pub/IucmWGqK4v758F3tvq7a/nP2L8yx8tL9qgSCIhRqyvx5NuDgJAa+/oMGwjQxaqb/l9R4Bkaqiv89vvxUle5a/ypEPXEEwbL9+h1+Ru3OUP+YU2JPe06E/2BgeyZbToj96vipyyMOTP7DW08PFb3s/EaUeezs0a7/mhtLbUSmNvzpQIUG/7Zq/uBmxiyIIpL+jF1zbWgqrv4WbodxGPrG/QB0puCxZtr+q7pX6WCm9v8fUyRLtOMK/Y/I1XLIjxr+BsQtm58TKv00auTwCG9C/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8L8ovNq8zsjtv6robTL6xOu/vKzlXomx6b/gmVvropDnv8uyrlkabeW/n2ZizTFH47+2aEUwOiLhvyQkvYz4+92/hL7JXZez2b9rH5k0uWrVv8rYmKaBIdG/HYwViUOwyb9eWS/xjCfBv5DzRi1tWLG/WbAoZzu1Zb+5oHiCqG+uP4CzOnSf4qI/hu6QwXgnlL+TehL3yX2zv/Ae4Se9LsK/k5wHnH3Kyr+rnGp+uLXRv1AiEBT//9W/FKzdUa1D2r/kM3fpyoDevyQx/eoNXOG/hdF3ePRo47/iCEk4wGPlv8NeaXy2Vue/jsIhotJA6b+dNu4P/gzrv+byskf9tuy//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r/kzw+K4WPev+Meg14BX9u/Kqvk8L9c2L+4LtTBH17Vv6/Zi97QYdK/QI52YzPUzr/4XBa3IUXJv5tY892s4cO/QytR+zA3vb/cZjk+Ce2yvwO+j6H2k6O/OrC+OzEFg7+JgnAwERWQP1+hUot5WYE/uBz5Fc6wnD/dL9PBFzekPyySzBkTGKU/HbQizO3RpD+ZpXvZj/KlP6mY+VOwSaU/NL6an4OAnT9ujaljxZR7P45XhVV7cpa/GKgihNjrqr/QhQ548SC2v607U3F+8b6/h1ph5MaBxb+f8rsJMkDMvxH9JogpjtG/2qrYyVX+1L+/F4qwgXDYvwzh9wT449u/X1p1BCPP4r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T/+Jw3lKJrmP4t6eeaUHuU/+F8vi1Sm4z/Qe34+9y/iPwNHzNWmueA/YMyiCHuG3j9yqWFmgX3bP9SwBxyJX9g/PN4/rFo81T8xw4TRCRvSPx5Bkvmg5M0/q9GefWi0xz+a5ak20yvDP4Ol+l7WHsI//g3HfYRHwD+0ytn3gcKuP7cdDLaaSZY/JgetB6SvoL8+qSGoVLqov67CwAmB5be/nSXl/MDjxL8ey88ON2/Nv11vaN/2vNK/xI0Adfqf1r+/pd36dFnav6VIcv62Cd6/jB7HpmaW4L96OpTrzwTiv8n+qEP/XeO/D8MLBeeq5L+eorRhmfHlvz74VPEMNOe/p/SXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r8+lsIEO73dv5rF6+TYuNq/BJJCKBG117+edEDAlrLUv5p490Ksr9G/2KA1LMldzb8xn+TIhI7Hvzzzjkei/MG/KmyQxwbVuL/CPK91Grerv8WutFVteYq/VDZk+eTulz+y5CBCMr+bPwIGO4rv2Ig/yhTUvWgyoD96pQLaREikP5Aoflh12aQ/eYi/1t3LqD8Y23a441irP8M0jQMIu6Y/9xk+1tgdrD9WepL8UleuP0j8qwezSKo/3U877Ui3pz/qU91I51emP2ZNFkUqgKU/Et3TT1HHoz9kMs4vp3WhP76nKRAQdZ4/brPQK8dLmT88rgjEtMWVP+Qu86hPgpA/CoqeTDl5qrw=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T+C53m+H0nmP4sPyIVj1OQ/Ug2Zj99f4z8y1WPDbuvhP4SEGYm5deA/14hoVQL/3T/S7PECZvPaPzTftLpt1tc/g+15Km2x1D9bKlLWsYzRPyv4u0WV1sw/EOk5Bc+qxj8Hpts+DKnBP46gtwgG28A/HE9+UyQGwD81+12/bcuzP8xpcfwTZZE/yEhdxYYclL/Ys2tK8z+qv/ism7V6Cra/u62KdRohwr+75rcR+s7Jv6JsKDL2LtG/wdCytHUN1b/OQL+HOfnYvwFJ9/LL9Ny/iO4XIO914L9wHu4DlHXiv2KcYVF0e+S/Qkud4ueX5r8caO3MCp3ovzjKS9Inseq/AAAAAAAA8L8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6b+LjAxtgwLlvyk+sgzOU+O/DDZqUc6k4b9PNTrmCurfv2DiK4r/h9y/prKX8TMi2b+TILN9irXVvw5fd6iXO9K/kpmyL8Frzb+CdqRCzEXGv7ztSfkV8b2/URGpQFmprb+gBoZYKZlVP3bNzqmWZYs/CjBpa5IKlz/Lgh9V6xGePyxt4ctYSKE/JC7dhs+zpT9C8r7bMS+bP/PJRrkUG5Q/zFqLnCwXnj9Nyfgx92yvP/k/9Y+hSrg/Oi5rhUe/wD+MBaoQjZbFP/znaJ/Uq8o/Uou96gMJ0D9WgO+xPsLSP9Z9XY9Hf9U/r+ygu64+2D/ZEa9Gwv/aP737efIWwt0/XFp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "XVp1BCPP4r9zzQGUa6HcvwxEiyWVj9m/eOebnrV81r913Yzf02jTvwhVeThPVNC/TvrGNbR+yr/XIGywKVTEvxHKkuK9Try/lWR6bTgTsL8cgtkVaGWOv7EiHXpwEKE/zDt83TkttT/tqM5mRQTBP1zaXhv2+cA/G09S7MNzvj9w1TbATS6zP2TW6LkGJpg/UdCM0eKtnr/PJW9Bu1aUv7eurhso8YO/Qsz+T7y2or8p3RMJPdy3v0lls65WO8O/sM+utYR9yr8UgrJEhePQv5oD8z8jhdS/150XB+ER2L8frqqxo6HbvzbEjJJIM9+/jEgn2fBi4b/AGIiaWizjvwkK5tS49eS/qfSXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UukvN+/G079L8HAdV/fVv1kFMnXSJ9e/uoBRDZQK2L/vjiVbuTTYv1B69/l+Tti/iupQhABM2L9NDVKZr5fXv4ap3GNt59W/Xxl4QI8N1L9O/u36xvnRv0qkBk3X+c6/REt+UKspyb9KSKzaJiTDv53kdTVMsbm/32brbh+mp7+8YbdZFkqJP18JtiG8IKM/o9/cuKEWpD/LsqyoI5J2P0dQfxDbTJ+/D/43mpMzsb/8e02YKqy6v+jUX+oAsMK/P21ndIbSyL9NXbOS0HvPv7oxNBXRONO/ZikM4uRG179/6eG7zbXbv5CrY7ZIFuC//AhcV6JT4r9PnEJt8J/kv25E7CN48ua/qPSXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7r8U0tKpD67rv6clT6p3b+m/gnL3QJEq578/G+K3utfkv/MZu3aPg+K/qqxYN7kt4L+4E+6OEM7bvwieBxs/cNe/CcOMUUQd078cPUEki7PNv+J8Ph32nsW/xug4W6xIvL8Q2N8VCg2sv4ByYQ4cd1i/iOLZ01+Bpz8u32LRfP6zP78Ad8e1tpk/zm3u9/xrm7+4uEW/N2S0v/evQmzaz8G/mWknNAzYyb9Gogxq2e/QvxK95flOyNS/mo5c0iJy2L8dGku8ndjbv00pIKCoCN+/Yr4Aeeeh4L/rEgQMI2fhv8vE0kSvFuK/+5G8/bm54r/An0nux/ziv1tz25/tEuO/XVp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6b8tipRjsQblv36jVZF5GeO//IXZ7EEt4b/bQW2WmYTev3bJQScCrdq/1mdktQDY1r/CSWwuNxvTv+1IaGzj786/NeU5E3enx7/H0m6drmbAvxPsMhoQT7K/x1vPCVhFjr+AfYUKpdmIP3sYoGRJ9os/ihmEBGEhkz/iG9YRzGukP6zX5LJH2qE/aD9wRb+CqD8mwtNEjG6pPx9qclvkR6Y/cy2zCq6Jqj9stcgACvCtP7vDkZ3dD6s/mMgnYPGwpT+ulZHjd1yaP4rw2wvTvnk/pp414GXpmL9WN4H2YU2uvxGoeqKmPbi/eOqrNoy0wL+j3vl9pFLFv4AMXOOx9sm/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "X1p1BCPP4j/X1RvkYLLgP/qgI8T8kN8/OvfVuqC93T/zfduBS+vbP4GqasqXFNo/DzgiZMw82D9a0sjJfkDWP9z0Pgr/INQ/4od18gb/0T9CbXDULr7PPw2j77Wcdss/fggTMmzNxj9sWwTasIzCP09cE3fe0L8/yaKFyOByvD+f67QF9DSxP5quZ5GpfHI/ao5i0Futn789JRuFbHesvwFe3rA3qbW/YoTuo5wBwr93FaR8Ji7Kv3nYkr5dK9G/uNWFb+p11b9kwDDDbL/Zv6yDVdi4892/DMWHs2bw4L+ZoUslrM/iv0y1vjJ2puS/25+n3WZ35r/OcLWAukXov9oyJX5oEuq//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6b8uCzxUQ6rkv9G3/f5y4+K/TfCA7cQc4b9YeDGqs6vevw79joXkHNu/REuEEauM17+NbYsx4/rTv9bm65VabdC/gZI98ZkDyr8YRknFVyXDv2Y829Pjsri/Zrc/FyUeqL84F7iBs9NqP40s88kCtoM/CWgKiD//mD84AfwD2GmhPyVl+rM4mqU/9cKakndwpD/PkTBMZhSUP0h8Qg2+b44/sny5efTNjD/osjEUUQW0P2fcO77i48I/AhiuRxgRzD/M+8gRjJzSP30Y+/czLtc/pm9fBEvA2z/QWasjBingP+cvKBSiceI/Gha2p+G55D/wbpjjwgHnP/jyuhZMSek/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "XVp1BCPP4r/le9h9qWndv+CFsMtpg9q/rXbDj0+c1794mfzS6LPUv8Z3YcvBydG/Dr5wWZu6zb/neeV0utrHv2iGXYMJzsG/mjpcrSihtr++gPyrL5+iv7qMf79jLpM/ZLGO2FxAtD8JlEAa6vfBP8n66gxc98E//njnfWfSvz9y7TNDzdazPyif01+C25E/blh5uZ8Rnb+892IQHT2Xv62h9/eV34y/Bk51zVGTgz+nOj5ZcPWVP7g6n09ftJk/pZu+AdvKnT+/mcmMt6+gP//COSHmZJ8/SkERhfiSnD/R6SCrqq+ZPzYEG1qhxZY/sLcBWTfKkz/m3IpEpcGQP8m3a8acX4s/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G079GyrNGLb/Uvz6rN1Zrt9W/Noy7Zamv1r8vbT9156fXvydOw4QloNi/Hy9HlGOY2b8XEMujoZDavxDxTrPfiNu/CNLSwh2B3L8Bs1bSW3ndv/qT2uGZcd6/8nRe8ddp37/1KnEACzHgv3EbMwgqreC/7Qv1D0kp4b9p/LYXaKXhv+XseB+HIeK/Yt06J6ad4r/ezfwuxRnjv1q+vjbkleO/1q6APgMS5L9Sn0JGIo7kv86PBE5BCuW/SoDGVWCG5b/GcIhdfwLmv0NhSmWefua/v1EMbb365r87Qs503Hbnv7cykHz78ue/MyNShBpv6L+vExSMOevovysE1pNYZ+m/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j+mRRcq4BTuP0w26kCyuu0/8ia9V4Rg7T+XF5BuVgbtPz0IY4UorOw/4/g1nPpR7D+J6QizzPfrPy7a28menes/1Mqu4HBD6z96u4H3QunqPyCsVA4Vj+o/xpwnJec06j9sjfo7udrpPxJ+zVKLgOk/uG6gaV0m6T9dX3OAL8zoPwNQRpcBcug/qUAZrtMX6D9PMezEpb3nP/Qhv9t3Y+c/mhKS8kkJ5z9AA2UJHK/mP+bzNyDuVOY/i+QKN8D65T8x1d1NkqDlP9fFsGRkRuU/fbaDezbs5D8ip1aSCJLkP8iXKanaN+Q/boj8v6zd4z8Uec/WfoPjP7lpou1QKeM/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D9gEiaLihPpPyyUvF67tuY/zZzp0d1e5D9/r0lEngviP9bsmVPudd8/HxJz9EsA2z9Q0SBWEbfWP1DZiD2AntI/eXMCtGZqzT/vSG8TjFXFP3e9a/K0Brg/Sm3E+jKAmz+AOpcLpo5vvwgv7ufVD0M/4FLPCSjUbz+yl4fixf6lPyYvca0Jsak/Vh3n26SGnj/PRJe+li+bPzAZXvCHuo0/gArIFtCZkr+TOXdnE/K0v8QU/u5FKMO/zsh3BSKzzL8bzMQmOR3Tv6TfGr+inNe/ExvMksLz27/BIJto5Qngv6RDUowrKeK/lsSQFpVO5L9Ignh1PnPmv56ZJjXPmui/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAACpFZnFoiVrP4bq169sboA/i7Dsf0+Gij/wuT1mv9SRP4c3Lo2y95Y/M7PmTyvamz+gr1n6O46gP74/aggR/ac/OOxwpKr4sD+UMsixN6K1P7mg2H0947s/N9a2oDJswT8IshpYrBLDP0L2+phKssM//lD3PWLIvj+YJ95efvq0P2AlZZ+LDF8/IB2wkZExgL/AStaDBnmAv6z94LW3Zo+/deYEQhiQiL9N2ebm0WWLvz0U9iR46YW/YRF56Jb8h7/NBtKDV46fv97YnSpbxKi/lMBWh1tqs78h+aVIRR66v+e6Gu0P3sC/CTiAcJKcxL/ceHCJOEvIv6VQTZz1/cu/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+IbasUC6bhv5gl7VMTtOC/dk6tjI+I37826RZ0akvdv+MB/za5B9u/OyerlwjI2L+u3O40LofWvy+XrT7mPdS/jTc4NxLf0b9kDjufx4LNv55Xh7LnRce/XoHPntFZwL/mNhUcPj2yv3U8o9CoHn+/WDznTGqGmT/gdlFuMXyiP2+x1Y9m0aM/+pl5YzMWf797ZaGEQxuzv1u9XCu0EsK/q/GqlTWjyr8cBOk88JDRv70eP+r1xdW/EoZzBgPv2b9OsAWSAhnevwRPMaWkH+G/DcFQ2+sw47/PQR5mcEDlv9gzbQzRTue/wcm+Zotc6b8K2sizDWjrv9z5zyQ1c+2/AAAAAAAA8L8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T9Wn8rdalznP08QiVtNkOU/UvvCFlPA4z8rVugxHQTiP8u8Gk82T+A/8tRXt/ow3T8bU+QKEMPZP9qd9xBxWNY/Bn6fLcP+0j+LrlKB5T3QPyR7UkKgH8s/sdJZU1i7xj9tmhu7QuvCP829UZ7taME/IoD1cHmPvT9XP0TZqVuwPwteqJAuB4E/5YNL09WflL/65hx3qEOKvwjytllBano/52GEDzbQlj8XEjRS4xmiP+gBqbcTw6Y/isO+eIolqz8mS1ihiBeqP7mDYMWpdqg/pvZzxbs6pj8cK94THOGjP0N43Hd8S6E/iJ+0dWpRnT9rRg+tOTeVP/0M08PgXIk/B1wUMyamoTw=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6T8rBNaTWGfpP68TFIw56+g/MyNShBpv6D+2MpB8+/LnPzpCznTcduc/vlEMbb365j9CYUplnn7mP8ZwiF1/AuY/SoDGVWCG5T/NjwROQQrlP1GfQkYijuQ/1a6APgMS5D9Zvr425JXjP9zN/C7FGeM/YN06J6ad4j/k7HgfhyHiP2j8thdopeE/7Av1D0kp4T9wGzMIKq3gP/QqcQALMeA/8HRe8ddp3z/2k9rhmXHeP/2yVtJbed0/BdLSwh2B3D8N8U6z34jbPxQQy6OhkNo/HC9HlGOY2T8kTsOEJaDYPyxtP3Xnp9c/M4y7Zamv1j87qzdWa7fVP0LKs0Ytv9Q/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "YFp1BCPP4r+6aaLtUCnjvxR5z9Z+g+O/boj8v6zd47/Jlymp2jfkvyOnVpIIkuS/fbaDezbs5L/XxbBkZEblvzLV3U2SoOW/jOQKN8D65b/m8zcg7lTmv0ADZQkcr+a/mhKS8kkJ57/0Ib/bd2Pnv08x7MSlvee/qUAZrtMX6L8DUEaXAXLov11fc4AvzOi/uG6gaV0m6b8Sfs1Si4Dpv2yN+ju52um/xpwnJec06r8grFQOFY/qv3q7gfdC6eq/1Mqu4HBD678u2tvJnp3rv4npCLPM9+u/4/g1nPpR7L89CGOFKKzsv5cXkG5WBu2/8ia9V4Rg7b9MNupAsrrtv6ZFFyrgFO6/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+nL7GVVrvevzwr3UWiJdy/aB6+96CO2b/vfdhrx/PWvzhyKKqkUtS/BrMhOk6g0b/wAfBQ0ZHNv6xhd9gNVMe/cRqLIgIWwb+JgGQtWAu2vy6+nX5nmaS/sNm5qORcbz8cy+mxwWWKP51DIBJJWYM/7GJMIHlikD9w4zkI+X6jP4L9Vvk8lqU//wxpH+xlpz/F1IR1qQCgP7XvdEdAk5s/I6NRZoQUsz+quSLjHzLAP80BEVOkxsY/Kn0CHxJDzT8439M6mtjRPwzYzn9wL9U/YHkLNW2Y2D/87cxgtwDcP/gTJaNbat8/rIYrz3hq4T+ksIPoRB/jP0kFsh+O1OQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T8MVjmdAUblPyzeunzrquM/0MBOYsUP4j+wGaLzonPgP/K/X6gRr90/w4piKYt62j8MyI+AYGTXPybq+O+3k9Q/GPAEUEHd0T+37RWzZ1fOP9P7h0dr68g/oykZfCLpwz+uvXiy7zrBP77DiLYbZME/VTso5IoXwD9EGHShA86yPyaDUd+L85g/HiLA5cN0lr9294NoZ8uXv8yfZWW10Gy/WnhTU1i+pz8gO6xa4GK3P0IbLIIOT8E/vz7o2lndxj9MTFYOiljMP83kbRnDttA/iRgpNrcc0z/PdDi8D3/VP1BSCeyq4Nc/Zl0h7yNC2j/fX1l4aaLcP7QPeL77At8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6T+mn28/TQ/oP0dDfONNueY/lvyWCgFm5T+ss+fT8ALkPwN2s5JXneI/bpwx2cw24T+249mPQKLfP9PhQ4zYvtw/hGUjDPX32T/NENliagrWP4t6lRT34dE/ecoe2zl0yz++swbaHRvDPw+ux8s7BLY/iITPCvfyoT/funF2dpegP00fxrxp0ac/LVRWhcT9rT8UAqoxNae/P/I31hJbicg/DL3bnU6G0D+wiDbyUKXUP+nC+LROodg/uIRfnIqM3D/hBEwalRzgPyfmDENa8OE/pKlDy6XV4z8ByGJ3OMHlP6f3i8Ftr+c/TbxDrsae6T+PlzbG/o/rP2RirXjPgu0/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "Xlp1BCPP4j9rnbu35srgP074LaySwd4/GtOVDQns2z9XmourKjHZP0OTUhuWhdY/LfewWGzg0z/8N7TWg0LRP3u9GM31y80/M1gOxX4+yj9vKlPB/7nJP80MKSrGssg/kLaSV9BVxz8pbJGPPbfEP+O+ZFEMrsI/ke1SDj/9vj+y2fscBMaxP4ht4SfFYYY/exLnI0C8ob/C4EEpNRiqv4QSfK7DHK2/nqHdsvnTq7/erCBdJHGpv+KCajXh2aO/IqCsa1WDlr8SC0BMMbZ5vzfmAHv0Mmk/k8yauP3Lcz+8sKkuCI5zP+HN4TIGk3I/14SGgrNgcT8MSAIJmiNpP7ENYSFWXF0/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z+vr+3HwVLQPz8reIfDSc0/HKo6CpPtyT+9TlOPApHGP5NeoeBENMM/RMyUa3fmvz9q7Qvianq5P+ta0X7bLLM/TvxSOQfjqT9d2mAJdwucPzC2MS/19Hc/+DISBoj1db9adImgsSxwvwAlHHPGq/i+4qoeefyvmD9WNZkqN+GdP8CrYxn8SaE/xEhcwwT5lj8xNINRbemiv7RkXKMfQ7m/9iVi/ezDxL/e1LpvhBbNv2QzNk62vNK/xG8izFLz1r95vchTNSbbv6fcxawRUd+/w6vxlMO54b+r8+mT37Xjv/+eylnjkeW/jzLey71m57/kJK9JGDfpv7t8QlRNBOu//1REEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j86Crno+p/qP/H5tf6Hw+g/DEY7a53m5j/8lH+6WgnlPyjn6xehK+M/1pJ1bsVK4T+sg7/sfNDeP00FCUv8Dds/CVRdzdZJ1z9fZU37FYDTPxg21/2hYs8/HhpXOlCxxz9Uxg2JGQnCP6/Sh1xkdsE/CPwIX97OvD9J9SeMMJKxP77MBcW/hIU/L81W1/I5l78iTGHkwml3v8DT6pWNPpE/gurXalN2oD/s5kx7bDmlP5TUaqI6TKg/tiU9vFfaqz8ql1XKJwewP2imfvo167I/Lf24EaJ4tj8Oj11kfcW7P5jOcG2swME/AdkFzbqvxT/lIkEvAqjJPzt1ubFdpc0/UekvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G079GyrNGLb/Uvz6rN1Zrt9W/Noy7Zamv1r8vbT9156fXvydOw4QloNi/Hy9HlGOY2b8XEMujoZDavxDxTrPfiNu/CNLSwh2B3L8Bs1bSW3ndv/qT2uGZcd6/8nRe8ddp37/1KnEACzHgv3EbMwgqreC/7Qv1D0kp4b9p/LYXaKXhv+XseB+HIeK/Yt06J6ad4r/ezfwuxRnjv1q+vjbkleO/1q6APgMS5L9Sn0JGIo7kv86PBE5BCuW/SoDGVWCG5b/GcIhdfwLmv0NhSmWefua/v1EMbb365r87Qs503Hbnv7cykHz78ue/MyNShBpv6L+vExSMOevovysE1pNYZ+m/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j+mRRcq4BTuP0w26kCyuu0/8ia9V4Rg7T+XF5BuVgbtPz0IY4UorOw/4/g1nPpR7D+J6QizzPfrPy7a28menes/1Mqu4HBD6z96u4H3QunqPyCsVA4Vj+o/xpwnJec06j9sjfo7udrpPxJ+zVKLgOk/uG6gaV0m6T9dX3OAL8zoPwNQRpcBcug/qUAZrtMX6D9PMezEpb3nP/Qhv9t3Y+c/mhKS8kkJ5z9AA2UJHK/mP+bzNyDuVOY/i+QKN8D65T8x1d1NkqDlP9fFsGRkRuU/fbaDezbs5D8ip1aSCJLkP8iXKanaN+Q/boj8v6zd4z8Uec/WfoPjP7lpou1QKeM/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G079GyrNGLb/Uvz6rN1Zrt9W/Noy7Zamv1r8vbT9156fXvydOw4QloNi/Hy9HlGOY2b8XEMujoZDavxDxTrPfiNu/CNLSwh2B3L8Bs1bSW3ndv/qT2uGZcd6/8nRe8ddp37/1KnEACzHgv3EbMwgqreC/7Qv1D0kp4b9p/LYXaKXhv+XseB+HIeK/Yt06J6ad4r/ezfwuxRnjv1q+vjbkleO/1q6APgMS5L9Sn0JGIo7kv86PBE5BCuW/SoDGVWCG5b/GcIhdfwLmv0NhSmWefua/v1EMbb365r87Qs503Hbnv7cykHz78ue/MyNShBpv6L+vExSMOevovysE1pNYZ+m/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j+mRRcq4BTuP0w26kCyuu0/8ia9V4Rg7T+XF5BuVgbtPz0IY4UorOw/4/g1nPpR7D+J6QizzPfrPy7a28menes/1Mqu4HBD6z96u4H3QunqPyCsVA4Vj+o/xpwnJec06j9sjfo7udrpPxJ+zVKLgOk/uG6gaV0m6T9dX3OAL8zoPwNQRpcBcug/qUAZrtMX6D9PMezEpb3nP/Qhv9t3Y+c/mhKS8kkJ5z9AA2UJHK/mP+bzNyDuVOY/i+QKN8D65T8x1d1NkqDlP9fFsGRkRuU/fbaDezbs5D8ip1aSCJLkP8iXKanaN+Q/boj8v6zd4z8Uec/WfoPjP7lpou1QKeM/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r8Y8jZlMRLhv7O0LD7P8N+/JELzIL243b9gvtKI8H3bv33QP9kMAdm/gXhOxD+E1r8fgLMhk//TvymSYXd1etG/YOM8LEXJzb/eHYHBR5rIv6elp2fdtsK/eKRblnheub9yHbhl3L2lv6NtcwNAemk/G5+PfP9smT8hT9kyqgShPyr4z21DTqM/m3j/xjzjjD9OYC16bGepv9bFFac2RLy/zk3JlpOLxr/AsJatmwDPv1fvzGdQr9O/KWAwHkip17+sDYdcbLzbvyQxozoWxN+/VbZL3kji4b9beyXnBNXjv3PhdRffu+W/AGf31DeR57+gUXqJlGbpvxQ8gI+OPOu/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T/QBBi6gezmP6gWPy4vKeU/fcezGFRm4z+HSr6Uf6rhPye2rgl3AOA/ZyQ4aGyu3D/lskQDX2HZPzh8f5ggF9Y/AF/NYH7e0j/bz4v11IzPPwAuZLxVD8o/DKXuKmZCxT+uavuv2ArCPwy2WYOEO8E/UfxdpOmtvj/4ra+H7HWvP00fOSzWFoo/6MmKNBaCmL/Cdnjq602Zv4EhqGjUI46/jpLYIeT2g7/sU8IbMLeQvwUKptedgJy/zWercXoIpr/l4QNVRdiuv7fbbTruW7S/KjKL8fV6ub/64bOxn7y/vyUBNiFvXMO/lA76UExTx7//Nll9o0vLv4fGS56ZRs+/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j8MX3b2OzbsP3oMG7WdQuo/KhRq+DhT6D/X/mjMVGfmP1TJQLlofOQ/DpIR5QaP4j99VUC4FKrgP8ptnyB2k90/2nna0E392T8cycqR7SjWP0KWHD0wStI/pvmJVV+bzD9acyD/VqPEP0fS8dCiP7k/PCxz7Te/pj/298XCbruhP223bCRKhqc/1cEZRmnSsj+54Z2E6RnBP0w+rXrgSsk/uSVbbynD0D+alENxAuXUP9l/R4Fr/tg/AkSTt4zK3D8CBocTnFfgP8rd145HSOI/A1MTObg65D8aZI+HEy3mPwv1pUITHug/a5IEfiIO6j97y6pbQfvrP/RF65rw4u0/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z8Ah2fCY+rTP9WqfTpOvdM/JSUFJNlM0z+dWMm2fqzSP2QPPEtn/tE/wc9EQ45E0T8WBeefQD7QPytZvxwz680/ROdwPsQcyz8txJ4vOPrJPwoc1AoTuMg/IkPC6Urexj+iCeoQUcrEP+jvDKpSFcM/dz1Pep0+vz9CzTqA0BOyP5QDdA/g1YU/dfJI4li3n7+Qysk/Vtqov6LHvgoTWq6/ykn3EN14sb9q8IY1uPyyv+AVIapV+7G/4rmmnSxEsL+Hk8XHHaOvv8p7G7h+J7C/JQ78fESyr79+ymbOTd2sv4hclgKp4Km/AxxXMeCmpr8w9mcWn+Chvzla6c5Zm5W/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "CoqeTDl5qrxuIR4FTD2bv3K/JVKrKaC/jgT1mqueor+H9cWpZvekvzRJVqfjJae/9yo70podqb84y38hF8OqvzM0lyJzG6y/i1Wd2i03qr9Yim/WUEKmv9wWV4+kL6G/4pMS/vGzlL/CS4SHNPtmP6P9JHawIng/SE2hfNOEoj+10AxBT+CePwzqbHuEU6o/Mp9GGllsoT9DBhAYbZiiPwzIHCsy2Zc/YxYODgU8kD/6Abmt4j+jP/6L9Khkmq8/clX+UVpJtT/touEiUWu6PyQBG4Azg78/XVi3pKxIwj/+14OUHNnEP0ZZWGCKcMc/x/R19wANyj+I74i5G6zMP6Ph4vINTc8/UOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8L9218TeR1/ovwauk+UF7uW/u/9WaW1847/B3ybXcQrhv2uizTgTMN2/2PYb22BK2L+Ez/+vWWXTv0BpGGGo/sy/TkV4vfE0w78UgmcyYeGyv2imPLj2FWE/55DvneSxsz/vxmJZXSnCP5J4LsRdX78/riKWZR76uj8WaXt0suKzP2jBCPl+X2M/Lm3MRWtwpb/GaiLLH1Omv2g7bU9TUbK/CmQKSPEIor9M8pQOh5OYPy4FEOoxM7Y/sqGH2Id5wz+N7AF+bT/MP1JJQztxndI/sg5DDD4n1z+E84rWjb/bP85ndkbIL+A/XwsN+MSB4j8tROLUWtXkP+LV89P7Kec//1REEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6T9zh/M3CXjlP0xnGvDcvuM/eDUpnLwE4j9k9IS1a0ngPwmmjBEmGd0/fl8mgJCb2T9Lxr44JxjWP8ajWDxLNdI/ghDVIjB4zD8cLKQ4IrTEP4/0J1ou1bk/6WJXx+G+oj+qJVR1H7BjP+ZEP2HaKpE/g06FsqIelj/3a5wQvpyjPxrzT4CY1KM/rTHwHy8Vpz8DeaSQCNuTP16Eg0Ir+Is/o5lzVHbol78rWOxRp0q3v6ll9tI/88S/tDSasxxazr88lRI5sPPTv4LEb7FaRdi/r1SFl2yK3L/gx7xMi3PgvzIcTgSrf+K/yGmPucSW5L+ejzUvW6zmv9373r6Kz+i/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "Xlp1BCPP4j9Wb1tEu73cP6gkhYOe6dk/5Jq1GBwY1z/W/xqmGkrUP1t5Dx3wgNE/OGekNJ5+zT85J4MP0Q/IP1wRIP4aUcQ/ODOqy7YxwT8buaCgNbi8P6f2lV9nFL4/ttbYvRp+wj9wfWGSHUzAP3jd5sL6rME/xKfzb+rKwD8/Cl/7OX+0P2I6my/7qZc/NDNyLM+Rk7/dctzY8pagv4ZBoT+QwoG/xuenbHx6gL8qwyifJCKDv3olX/Yxq4y/2CeczP72lr/2PsNhdkSgv/kCMjEwlqu/W/AFQy08tb/yEttnxu27v6qLJTJMV8G/3U4F+ugCxb8LcAHz36bIvza7ofsNS8y/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r+nL7GVVrvevzwr3UWiJdy/aB6+96CO2b/vfdhrx/PWvzhyKKqkUtS/BrMhOk6g0b/wAfBQ0ZHNv6xhd9gNVMe/cRqLIgIWwb+JgGQtWAu2vy6+nX5nmaS/sNm5qORcbz8cy+mxwWWKP51DIBJJWYM/7GJMIHlikD9w4zkI+X6jP4L9Vvk8lqU//wxpH+xlpz/F1IR1qQCgP7XvdEdAk5s/I6NRZoQUsz+quSLjHzLAP80BEVOkxsY/Kn0CHxJDzT8439M6mtjRPwzYzn9wL9U/YHkLNW2Y2D/87cxgtwDcP/gTJaNbat8/rIYrz3hq4T+ksIPoRB/jP0kFsh+O1OQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T8MVjmdAUblPyzeunzrquM/0MBOYsUP4j+wGaLzonPgP/K/X6gRr90/w4piKYt62j8MyI+AYGTXPybq+O+3k9Q/GPAEUEHd0T+37RWzZ1fOP9P7h0dr68g/oykZfCLpwz+uvXiy7zrBP77DiLYbZME/VTso5IoXwD9EGHShA86yPyaDUd+L85g/HiLA5cN0lr9294NoZ8uXv8yfZWW10Gy/WnhTU1i+pz8gO6xa4GK3P0IbLIIOT8E/vz7o2lndxj9MTFYOiljMP83kbRnDttA/iRgpNrcc0z/PdDi8D3/VP1BSCeyq4Nc/Zl0h7yNC2j/fX1l4aaLcP7QPeL77At8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D+SwjxldmztP+aFaG3mces/9s7lozJ56T8nJhFT83vnPy40HPbYgOU//Ht2jNCA4z9HhVSHUnfhPxhnYTuqvd4/LqmOUvhF2j9y17Gk4+3VPwRAVmjlytE/ciuAAnKRyz9ybKerExPDPzPp3vTnk7U/iXzC2BKroj8jWFdy8TOhP6HG4Vf6fac/XKWr/0Pusj+xntTlEx/CP1H6N7Ka+Mo/fgje0PoE0j9BqCSn8WXWPzNwZgMmbNo/28vJr+kG3j9LI31gJ4zgP/BA6nQ8/uE/FNcWPZhZ4z9SSxb6O6rkPz14YX8a7uU/AMZ8QsMI5z+Not0rgvPnP9uhEhzJ2ug/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAADDdJUspKGhP0iEEqg3Mq4/D7ka3RZhtT/hC8QthPO6P4dVPVslSMA/bcFTQWT7wj8EMMJb3a3FP5rUgUE/JMg/HmCNWnLryT+9BByi877JP5ApGm7158g/IWSdPVnOxj+SiRBNo9/EP6UxxsHi88I/yYs089xJvz/c9h+dAIexP4H5S7ZkG4M/XE+bqo56ob+SAO68jViov1+RdZb4xam/aZXQUVZlpb8QjcBfkCOXvwHtiyk0EIY/k8DmYxssqj/+Fce5XQG6PyiCoYH2qcM/vF1f6FqNyj9EZmPeTb3QP28/u5zBPNQ/qVWYaBrk1z8Wel1FOcrbPypPtrx0sN8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z9tVo6H7o/QP5sOQyAJfc4/RfRsIXnPyz+4znvt/BvJP0OPV5NzZMY/3RJwEkOqwz8emOHctOfAP/ouxDZUa7w/tEM+vdxeuD/JLpugGNmzP8tSQGoWH6w/uHFK7IqpoD+Uo8bKektnP4CpMDVZIy0/CDxVKAIDkz8GrvV6FfCiP5yi8hUNyqA/ykkoize3nj9azh3lgHyhPxiHEMK7ZZI/D9TONFZgfL9LKxMvr2Ogv0PsxPJTOa2/cXoKQdMftb8+376LSnO7v8H46UQl1cC/+xc2sXbxw781t8HWIJ7Gv3BJ0T/VOcm//V3Bj8fWy7/RXNmJfXTOv3oRZAV/idC/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j8BHAGg4rbpP1UGlbct1Oc/TI0WDY7w5T/xYijARw3kPzv/GWvpK+I/jQzNRD1O4D+XU98gquncPz6u2NY7Rtk/ucGUI9OY1T+VCL4Bz+LRP7e5CuL+ucw/S3wXtX8pxj/81pQoqyvDP6RJNyowv8E/aA2C6Q2Zuz8CTNz+6QSvP3A/01Z1310/IaXlTsGJmb/5YCbYA2Kov1LPwIXxzbS/hPY+a7Djwb94yP4E8p/Jv5pLUh2atNC/fUQIX5+V1L+I6J0agXjYv+geHoGlTty/QC3ail/737+EZPoLMN7hv4tY/JUryuO/9JL3wa+55b/LIL20e6rnv0i2zc+9m+m//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "X1p1BCPP4r9GQfMo58/ev2jM3OSwb9y/VHxawzIL2r9qS7BbDKXXvxpssGBbPdW/S8OkyEPT0r/itXnxOmbQv9MjexER4su/YGctaPbHxr+mZMf0QJ3Bv4RHJLa4gbi/fnjhqIW8qr+Acktch5tkv1j3rEdxW3G/QMRKl0BWoD+Kv3uI0S2mPzIHKFTuSZc/Hn1LTBx8pD9Ud8XKSDGbP2fA6icb44A/ll+uVermfb+XTg+TNZuaP2K6JJ/8GbI/sIbRcBnVvj//rn6OrQPGP9z/7kLM18w/uIHU4lQN0j+YYRU4DvbVPymJ9GjiEdo/cuk7UQNB3j9PeW3FEDrhPyEPH7hUVuM/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "p/SXm3fj6b+30DGwTsLjvyoiCs+6quG/rT6XjlAq378nAW2a3QHbvzLe6bS83Na/Jjm2M6S80r+AJnZvqUjNv8N2EICpGsW/TOjIwpMDur8WCEn7/eujv0naDS6heJY/uEfyrTEetT+Tq5UMS7rBP6DZBTLbor8/t7gIbQlCwD+2BIAMqPeuP/xHFEd+h4M/DPKGeF2vm79cFYUExOSfv4Clh62evZC/xvCxqPoihD/mNffLkvWxP9Q07/mLgcA/MAelQp7Txz/yK+IH+dTOPx70Dyz5j9I/fFe6rH8H1T8a6PW230HXP2OZk0I7dNk/NzvVYBen2z9sKlzr19bdP1veOqFhAuA/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D/UcX8dKtDrP96kC+7Isuk/Rx+FDySX5z9PM9ozP3/lP2lVXqAJbuM/TQVuQ4JZ4T/X4JzgPmTeP4A6t+b8C9o/aepmCT+o1T912YmOhzDRP+uojrj+Z8k/i17ITsA8wD/78qLYfIWrP/U84XkdWIg/b0Lje4SOmT8/NzTmMtKhP8dNEvIjLKQ/7wbBJjMOpD/aqT4LZAOxPzx5DVOzELo/KIFqX2fHvz/Cyh/N3W7CPxJGXOyNuMQ/U9dh1k7uxj85HZhpNw3JPxjDa6BmKMs/JYQIfI85zT/Ct77QtDDPP6DDn3ejV9A/uC9NhYwV0T8wZDb2b9LRP3rnX4OJjtI/UOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAACGmaLgSo+dPyHmB2uSx6U/AkL7djXLrD9ZqabmFvSxP5DabCMrv7U/TxYEfWnUuT9QBYrpc5S9PxRGP2ZkzsA/sNJAZdOzwj8u50DoZzPEP6gucOP5TsU/Q6oO+oZfxT+Dq6RXX4zDP/okBQgSssA/0FIqvDLgvD/U78fa70GyP965zOUR+H0/pgA06T8znL+JnHBcfh2gv/JPyDv385I/PmzJoHpctT+9iomd5SDDP/8aWAxaocs/4q3ULggR0j/VWN95QFLWP9PeWl1ek9o/9Fd62uXT3j94rG7XgYrhP8UquusgseM/3A5DvlrX5T8JZGQ6Qf3nP3L7jGPdIuo//1REEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "X1p1BCPP4r+wreEALO7ev0GJake1jty/s4uox10x2r87iipjDNPXv36CcmgvctW/FzhsIl8O07/7cj4j66XQv8X/3bWmUsy/0A0yTIgVx7/UAzRhH8DBv7YL/7aYj7i/rMUPRLSpqr/Ez+6SwY1qv1xx9N9qS5U/wS/JmvA8nz8qRAh8ZQiaP0RxpnoDYqc/YUsAXXkOoj8+Xs8MlMKhP+TcQbRq95c/NCTSFX/lcL+OBUck4Taev+lrv2cttqy/kzTFrAYXtb86LMhhgbe7v/Y64rMa38C/5yfL/Dydw7+Tnr6Gc0fGv0lTfEaLysi/3ysjFPx4y7+R6bTAjQbOvzGunlI7VtC/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "p/SXm3fj6b9fQQ1ZFrjjv8EOSBXRn+G/zPAnJ/oN37/9XqzEed3av0RXU0qBr9a/Z2OcKdWE0r9bxFHtIcHMv9yPJiJulMS/E+BUr1cuub92jFqMNuqivx57sp15GJc/8Bp5KA5jtT9M/JrkFafBPzsHS9xsk8E/0KTzwKvuuj9CX22eAVayP2g6dt3FC2I/bi8TPn0so7/BQwmdnl2mv8QHHosFnrK/2yiIpMsbnr8kzCzXm8KdP501QLezErg/pSrI+NPAxD/DdtPCP6rNPy+O3hDfW9M/1Wep0P/u1z+42dE1qIfcP917EUdQpuA/BdCS3Hbz4j8SL7zQQ0rlP95F75lnrec/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6b8tipRjsQblv36jVZF5GeO//IXZ7EEt4b/bQW2WmYTev3bJQScCrdq/1mdktQDY1r/CSWwuNxvTv+1IaGzj786/NeU5E3enx7/H0m6drmbAvxPsMhoQT7K/x1vPCVhFjr+AfYUKpdmIP3sYoGRJ9os/ihmEBGEhkz/iG9YRzGukP6zX5LJH2qE/aD9wRb+CqD8mwtNEjG6pPx9qclvkR6Y/cy2zCq6Jqj9stcgACvCtP7vDkZ3dD6s/mMgnYPGwpT+ulZHjd1yaP4rw2wvTvnk/pp414GXpmL9WN4H2YU2uvxGoeqKmPbi/eOqrNoy0wL+j3vl9pFLFv4AMXOOx9sm/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "X1p1BCPP4j/X1RvkYLLgP/qgI8T8kN8/OvfVuqC93T/zfduBS+vbP4GqasqXFNo/DzgiZMw82D9a0sjJfkDWP9z0Pgr/INQ/4od18gb/0T9CbXDULr7PPw2j77Wcdss/fggTMmzNxj9sWwTasIzCP09cE3fe0L8/yaKFyOByvD+f67QF9DSxP5quZ5GpfHI/ao5i0Futn789JRuFbHesvwFe3rA3qbW/YoTuo5wBwr93FaR8Ji7Kv3nYkr5dK9G/uNWFb+p11b9kwDDDbL/Zv6yDVdi4892/DMWHs2bw4L+ZoUslrM/iv0y1vjJ2puS/25+n3WZ35r/OcLWAukXov9oyJX5oEuq//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UukvN+/G07+f0Mms54bRvzI6Fu+EedC//fCaDfDOzr8BU3pzxKLMv2P1rOAybsq/aPd3xQQsyL+yzgLoz3PFvzoshiw7bMK/5nvZ8NuQvr8bBsixuQO4v+Lm3mwo7bC/knGfjBQ+or9svXhx2Otpv/bsynxXv4A/SjafeuFfoD8+09YG322bP9DB63v00aY/GUSgeoIzpT96FEwa7e2mPzJyJ10NE6Q/hv9HuvUfnL+MLyZgIOS4v+zGYYPdbMW/NnVv6T9vzr8e+wf1xLvTv4sDIEH9QNi/G463zwWs3L+BYUZoUYHgv9YWfCuKq+K/Rbx2ZbPU5L+/xVjlAv3mv03btKGKJOm//1REEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7r/xfRhwMa7ov1muCCw7WOa/zx1t18MC5L99OY9Nmq3hv4avIAYfsd6/us3sgFUJ2r9S0sJWKHjVvx2iM3Ae6tC/X8Bxqs26yL/OqSP4YkK/v4453T7QVKq/SJKZhLxskz++yCa41cW2P6iggowd2MA/zIbiZjGkvD8k97XUgm+yP+hQhIsVc3I/nNVwhvICo7/ZMpwqqtOnv1VQEuxV3rO/Pz2mgUNks7+QJEPBQ/2tvwvQqBLB+aK/b2JpFqF/ir9LJwcM+0qIP86/o12Z76I/r82LBW7RsD8zhkcdV+e4P1gtsxBlgMA/46+9dEmOxD8QmCXNG53IP8b1CkdlrMw/UekvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6b8jnGQEIOXkvzpbHAo6KuO/KfjK4Ntt4b9qt/8+pmDfv1aDIdYP49u/7PbaaGRi2L8NzEKbBt3UvxU+CO2dTNG/FU2JU9xay78byBs29+TDv6vbdooF2ri/mxzVpKifo7+sBrcZ9wuEPwOyte3qYHU/7T6bLywGlT//qIlnVr6WP1dAM+3wEKM/srBrz2G+qj+Sx8IcK82dPyIDJy2kQZY/jozFnZ3dkT/ndiEPa46wP/hSgjExOb4/edNMxFxKxj9FLEj7aW3NP3TGoFzpRdI/1ddr2fPi1T+MeE4jtonZP4Ab9mVpN90/DDXCBPl04D+gaYXwEFDiPwhC4HeLLOQ/p/SXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "XVp1BCPP4r/kVHAyR+Dbv/Wt35y9h9i/06W03H8w1b/9+vRl/trRvzT3oytzD82/6UGw/3Rvxr8UonjZmrO/vwMulWMYh7K/m91szoeElr+M2P2iFYqaP4AUf4Cl2LE/OpSI8/41vD/KOCD1XWDCP+m2ujVgxMI/AN/iJg6Yuj+dz6FxcLGzP/QcF4k4oXA/Aao2VHSpmL8KRJfItnyiv6BHhw9wwl+/iaFU1pNKb7+EKt4E3DGWvxy9z1/ycqq/HVW3DdCytb+jq0wR1S+/v3DQF3VBHsW/F1rtXOjgyr9Vd39QJlzQv0UXuW8KTtO/x4BsGQdE1r/CHqv/ED3Zv3ENIv5fONy/YFp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G07//2zvqyG/Nv6Ka/DMT/sm/wR6IdhCJxr/blYoFLxTDvwBlynLzQ7+/kF+ARfppuL97YT9z8qCxvwsfmmP5CKa/tGH1xXE4kr+Yc7FiT/h6P4O/2MC2mJ0/987/qSZRoj8i8Sz2y76aPygf7kuCmm4/dBIwwfldnj95pCaizgCmP4w2TWDu8aE/FIAOsSBkpD8ihXgk7IiqP2ByO5V0GKc/czeUaocnrD/spKF3GuyuP2ywS/7iPrM/7Phg/CTatz8yalM6oa68P4iLVy+r08A/UWVqd/Jowz9Or5Yah/3FP2wQ/wrIlsg/PGOSvXsyyz958+UK1M/NP9Xt8lqjN9A/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j8bczYwGRXpP3YJC2ZCOuc/b97FzOpi5T/PX8kYoY7jP9EIbg88veE/5ucyycDe3z/1CvaqOEbcP/9PTpeVstg/YWSXsb8i1T95h7EpHKXRP+zxpqZfiMw/JTERE420xj8HKh5xzq3CPxJosdoZYME/7Q0fRkOgwD+YYoLiXia0P2LGHgKyyY8//YQyW4D2kb/uZyvty1Kmv+NNRT+TwLW/G/vz70/ewb9YUH0rS47JvzOjupq9bNC/mUCHfAYq1L+FjVN3le7Xv84YuMJet9u/H9xRNUZ4379JzPCZFJ7hv2s614x7g+O/USv/sQNr5b9m/qlRIFTnv3j/jJxxPum/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j/1cHo/lxfrP30WQwl2hOk/jHnSynHo5z/aXmtrgkDmP5TAHr8hiOQ/BmnFAoXE4j/F4Q8Fgq/gP7Zq46xD+Nw/diay98qA2D9rUK090BDUP2xIUzj6yc4/8r6DTUrNxT9sRYlriGa6PxTdZuplW6M/2d9/DX9YoD9oqunexYSgPxjpk7T8bKk/WFS1ozafqD9U/XlWwIu1P6KDGbyprMM/kgp4ODg9zD9oVQucV9XRPzs3CqeJidU/u3w44vKb2D9XzTnHwGfbPyQnQpwPA94/XJ3Hdm8S4D8+9M3s8ZzgP3n0W94pG+E/jKO2VBmR4T93oMQVoQHiP9dCqnNCbuI/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G07+wppvd28zDv3wxoZopFLm/ODnqnn+Npb9obaOTOv+JP/16QJm8qbA/c8BUCbTHvD/qVkjpO7TCP+bqcmGQUcY/Hwfg9hQIyT/8Y1TfF0TKPxn2IrxHcsk/qCrtiOVTxz+loEoPfVDEP6R05sen6sE/6PFqhjkpvD/JP+7t00SwP+0kzO9jt48/kDMA+tekor+3Idf9l+WpvyRUYyMtRrC/mgLOPeQNrr8Mj2/tsd+fv5gd8MUPOIo/lj3gjBwksj+UIs9bfsjAP35Xs/+at8g/zQMJIgSK0D8W5RIMIizVP2SUf2m8z9k/uhXto5tz3j/4xe9TF4zhPwtIEgq33uM/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z+IwMLP42PRPzBFlxGjNc8/2m3m1PTPyz+UWxjt79TIP170A/LG3MU/fBddpqXrwj++5Y9CHxnAP44WdUtkJbs/8WVXb2pPtj/BU62JnAyyP0kdLCSOoKs/LkYLVjsWoz9GeBpvQwmXPxKGvVLEzII/yusJmt3mkz8HidJ8UH+gP5RXmrFaEJs/hFWv8FtLPT/G60zjkBGRv9HxhDhuc6C/etT5Fiv5p78ZUldmgaSvv7viGjcX27O/uRP6Iq3xt7+aj8LpMJu8v7v2TXLquMC/pXl1vglyw7+aPjUP8EbGv54IH8pMI8m/UPAue40CzL9pDsrJrVXPv8DzFZbJa9G/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j+5LuBMNW7sP7l0D36Nu+o/rFXRlmgD6T8VsP8bfz7nP3zV2ebieOU/Tqqd+wey4z8jdv+es+fhP85TLOkAFuA/aU01e86B3D9iON4PksnYP5aAH596CtU/JxLGqvBB0T9BtCyYRtDKP+TGbD7ma8M/F0lduwzyvT/ATT0iruWwP2K/idAc5aI/Yj6GoG7utj9QLOfDL9LCPwrlZ1ijVco/mPPwzt320D+mRWpCs8HUP3XIVLTchdg/0w8j8HFF3D8W8a28OfffPxqHn4pw0eE/qFqF0D+f4z/L2mCP52nlPx6VqD4vM+c/WRA+q6r76D+gG+BQmrbqP00BmHvda+w/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6b/Zvfb5FFbov++p1m5I0ua/IxrxM2I05b8AQdBZ4Ybjv5ue8xDH1eG/eBXkULAj4L9yrm/GDszcv6rL+ynkRNm/HO1UkFbL1b+uB5//6V3SvxNN+kXvSM6/mTXNZPn0x7+CAwj1JBvCv6sapsqVgbi/n7+fdqw6p7/fTnPVsGeKP6RpXXPghqM/HX4zM5b5oz9WcgrQNhJ0P5sWPt3Tep+/vzKP/O5hsL+sCPJCA9C4v5PZuOvHe8C/+ku65peyw78IY79hPnjGvx6NH9+58Mi/VZudLvgly7/gyoj8MRnNv/22Ru5pDs+/77w+JLiF0L9wx5C4U4fRv2dtFfNTi9K/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "XVp1BCPP4r8xejBx/Qfhv81UWOiIIN+/WLo+E0pv3L8BiGUWseDZvyDPwOJ5Xde/Mmd06cLg1L+AUcYb1obSvy39V/6uN9C/iWhBzbKxy79AanKTe+rGv11Y6DKrkcG/7ZN0WLUIuL+nXKbKacGmv3ogw/OrhXI/6/ic8Hb2pz85K0qZKwy0PwxT35sSNpg/8hIDpeFBnb+kgG+fZai0v07O21/52MG/vDZDphc/yb9EuhhmOVXQv2uqtZfeCtS/9zfCuY/J179mZoCFSbfbv1C80Ll0s9+/kmwiO2zc4b9sW0Z0VOHjvzMMz0g86uW/t0kahd70579Xh5OnmP/pvw3fxoIAC+y//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8L9Bw9oVTCXqv4EEfc3n8ee/3lXR002+5b8gCe64eYrjvxpJyApmVuG/+OGf3GhD3r+zOQ1r5NjZv7z1wOtCd9W/JM1/JW8X0b/tRZvho2fJv+MkG+uckcC/8+bY2Dmtrr8gv5iiJto/P1TrAJbcMmC/00PTj1FImz8kML/9GTGhP/QOFqLqWJ8/xbKPYpZgmT8x7MbD8/2aP4LSBmcp6Yo/zS4D6wu1mj+MQkcaB6SxP6h2LVnj9rw/JlEU9hg1xD+3QoOHRAXKP8tDAAb7AtA/aRvWGMTl0j97Z7hf9ZTVP20c2f9xJ9g/6YDrW2Ks2j9AaWeTeCndP8BzuOrun98/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "B1wUMyamoTxbW3kOe1yCPy8UVqgrzY0/4n3J/8KglD8m2f98T16aP9PlQrTtD6A/Uth9wdbuoj9KAv2Oe9ClPwPJ9T/OXKs/4qWPMZ7hsD/YZMmmcja0P5O6YQ9F07c/W9GR6QKSvD8oq/RolvnBP4SQAMWFksA/WWyi1jowvj868DvvLzmxP4Hh38OKbog/xZ5w741Jmr+OeoLv4wOavxJwaF0Je4u/SOC7u/Yfpz9CWotn+BS6P23kvsq878M/ErdL+fWwyj+ySsOH0bHQP+D1CbOXBdQ/sn+Bcm6W1z+h6+U6EkHbP1MN2qEH7t4/miohAVRN4T+3hM5KoiPjP5BPZ34Q+uQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6b80Sz5OjhnmvzXwxRV4fuS/nP/zWrbi4r8ku5MqwkXhv6rc3dF2Tt+/m+F6YlMN3L/NTOzPZZ/Yv4XU6M6HFNW/EwWLQ2h90b9u7L29WqnLvxSlNyj0HMS/pr9MrVVDuL+uxIjvHQeav5sHtDuJ1YY/3prAftsckT9u1Vbbvy2fP05jhb5hfqE/Lb2i6uFXoz+8RR82dkSTP+hl80RPp6O/nFXrpJ0utL/cpDzGDCC7v7Pzetixw8C/vim6eQzEw7+x1oUKNK/Gv+QOdA98g8m/BFAiKirjy79HFAvge6XNv9rkbwc9Xs+/r4iHY+SF0L94kDpWk1vRv/sxsNRAMdK/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "XVp1BCPP4r9peTPE0fbcvyF6AZyvftm/arfJHcUH1r/xc3T2hpLSv6P3nrAIP86/vYXf+vVfx7+aq70LsuHAvxUhkV9qPLW/E6wzNv+vob+j9M6+f7OKP1V3zKCyH64/qbpvkQLjuT+1ajtMAQXBP8MUsj07dsI/PoEfxYnJvz+o+0/VzQKzP1Jre/c6NZA/3vjT/G0alb/h73itznqmvyFWHz65laG/Fbsi5btxij/8OZPr4rqyP63+6IC4o8E/QZBYKaU8yj+q+kK9F3zRP1qecdwC6tU/3InFXSdm2j9AAsbDyu/ePy3zUiU7v+E/YHqq9QYK5D8trJr1ZVXmP7APSfFDoOg/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6T+M4mU8O4zoP8cONRbqkOc/XRpmiFuT5j9CG3lZnZLlPzh5Ea8OOOQ/EsJWKVXZ4j82mFFWSGnhP9SGSRx44N8/PPsGXJ6p3D+CbXDK5kbZP0Wnzk/NWdU/spZywhJO0T9Lt/KYCUjJPygSSaOMJsA/4ECvKybnrT/8QTa5RR6kP3R9bitNQ6Q/4Gy/zNIoqj+RtX4WLrW3P2Zj2gBClcM/nAqXkeDlyz+vsHRJhEzSP04jCIH21tY/sPK5Aug32z8OEAwbCPneP2PD/aoXeuE/W+Gt4QJt4z8wzwZOK1zlP1devvEdSOc/l3M+h3kI6T8F0GzF35PqP5vSeuZJNew//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "YFp1BCPP4r/XdKyzioLev6lOOCcVY9q/mFIRL4JE1r8NDGa/WCfSv7quhj976cy/mNs0CCaJxb+PSZF0Say8v0C/66Diyqy/dq15mt8Rbr9yILidFf+nPw/fKse+UrY/IZC6waVyvj8cGo/vf3jAP50lo99CQME/9ueEDPCpvz+Vxx13v+yyP5oy6dmFXo8/MxhTLbTGm7/sb/mcHMetvySTTsps4LO/aoaqlqhPt7/5fqfzPpi5v8pSAdbKuri/nK4PohGPtr/OFNQ+p6mwv5QUICUcaZ+/FzMFLn+PeD+cpPbtyDimP1pMDYxM1rQ/v7GUsPsgwD/6pR73UW/GPyCsPssLt8w/T+kvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7r/dQ6i/VNnqv0CKwHC5n+i/ALrzXmxk5r8E1ru/ECfkv1JK33vG8eG/3vtTood5378FfNUUhxPbv2Ln4kFlq9a/sMxHF6pM0r+mAHY0LufLvwd80mcsX8O/KGjWfxILtr/k0NW+hriZv4iNoNJwgoQ/UdZfLIZdoD93zPwNbCqgP5JsC7JW1aY/w8t3n8Z+pz9E+Ok6DQKlP+QmTsqUOo4/izhwZpocmr+0e8W4OYKzv+aYRBegssC/R++b1iAdyL8q4wKmj53Pv8GdtztaqdO/qdc5YWuF178pCCfPn27bvwe1k9W+ZN+/B+whOMHb4b93xBi94ATkv94mKWKiLea/qPSXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "UekvN+/G0z92oVCft1rVPxielXsXuNU/PfgpT+QP1j+ZTlDdel/WP1h+DTauwNU/ys0p3ysG1T92NVCvWyXUP2lGJZ2xMdM/jlqkwcYK0j9CawKwu8XQP28Z2jFyh84/7U5sq54Zyz92tYO9t67GP9vCRWU10ME/bN9bZj60vj8nScB4k9ixP0oJVLppw4g/0pxjAr6SoL+P8eIYg+G1v+v278e7isO/c7kcnZ4yy7+jS8/VquDQv7M5bo950NO/fvLwgjZo1r+tcA+1K+PYv2Jtk1duLdu/KOxz52xo3b++781GCoffv7nEXlLMweC/Ay12ZwxP4b8lgZ/Ix9nhv68Yc3yhYuK/XVp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTwE974soMaTv89ZLaeTWJq/tMibETB7oL/vhjOo+c2jv4FIJN7wF6e/+GzpC8fNqL/evyU/nveov9LEBbmdJKm/x8u3wnpOqb+UJsZZOlmpv2oFsNKv5ae/xac0dFjnoL+CsA0kbD2Jv3vwH9Rq3W0/yr6TP1Xtkj/g3oOYGM+aP0IFyp88Jag/aOze4obIoD8Sqx3aOCqdv5Q7YYIw9ba/9CA+XnzXw7+9P0VQrArMv/g6t+MJHNK/JWH3SwM+1r+StT+JGz7av+La5f9CMd6/K0vb2IQP4b/qvP1RBvLiv7+XBeD3yeS/NJ2WXTGf5r8QWJ81InPov3H6iPf+Req/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D96pQND8ofrPx51hQ6rfuk/AALhWYl15z/KwCaOBG3lP4TyjE0DZeM/Zij7wn5a4T8CQXvOR6neP9FAICg2oto/x9Kumsai1j8N+oV2SrDSP7emlXwBxM0/uqw7AT71xj8i3w103HzBP1ImooB4wsI/J7wjPyUevD9DSW+zmQmzPwyVIMKW1Hk/4LjlbkVpmb9J4MkxDOiZv7a3aSLlzoW/Uzumc9h7aL+9ydvfZiGBvyC0mI3vgZO/SSwOcWu4ob854uG7N1aqv0cK936IA7K/V2I1dLwWt79PSJ2cFYe9v0qfOYuAWcK/RdfcM7wjxr/TEoah0e/Jv84alzJctc2/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AFVEEw5v7r/RJAHcqZnnv/hDO5MXauW/Onjlte87479VVFq6sQ/hvznGjUIkzN2/j4shlpuA2b8IuKd/6UDVvxbQ8AzdFtG/g+sBVPdYyr9Qt88mocvCv9C1KthEs7a/TuLOhn1Qn7/tFDIaCSl0P36o8DAPYoE/6Guqtgq7mD/iKGmyG7aYP9wHfCM7iKg/1Z2E5iMGqT+eHS4/p5OaP5o+XQmh74Q/6apGz4Vrlz8YrB5Blai0PxyrU8E3msI/BA7jTsATyz8UgqVFRtrRP5akWb8qCNY/oZhmefs22j/gY+8Px23eP/HkntCTVeE/xwlYFhV24z8dh0X3e5flP+ibXEb2uOc//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "TekvN+/G07/QlnRhIiHOv/8/FT4Qwsq/DA7QNShix7/wB26GHQHEv7Z/FNA/nsC/vsOCWCRxur8pFJupjZyzv5pJ75C5w6i/uGQ0wLqSir+I1YKSwVSZP7zLjCnC6rA/u4gL8x0SvD/yC0ybBurBP3ocEWbJ1MI/vnM4ASF4uj/SFgMUcrm0P26EH+R38YI/qKhBmi9PoL9+kK2lI42hvxPR4BgGt5G/2+9krj89hD/iqnWloamWP2/mmr4tGZQ/IMUS37WXgD8e9rYqtw9+v8ZWEr4qE6C/jGOUkHXerb+K814zHDW2v4H65Dvmqr2/ZYjV2NGdwr+dDvBlpW7Gv05ds6dMRMq/VOkvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8L/KAhXaT2rsv/NGF1r/Muq/GHLnvy78579QOb1wScblv2x712w8lOO/TenqJs9i4b/ZSomVt2Pev6AX/Y4WA9q/7aHEYKmk1b9tpgvDkkTRv0Rj6MpAvcm/c+jIlxrIwL8VWqy/LLauv8pN4n1ZbGM/0GenmCBqlj8xezW0dHuiP24SmuYDd6Q/oStNjbKFgT84WQwT+UCvv2PeWDQ9mr6/KKUiyLr+xL+M4bFlhq7Iv7j7YxxZ5Mu/YfhMhWdszr8m53FckWHQv0AdfJFPYNG/bEVdzPVK0r+wwiRiCCDTv2Od5GHzvNO/zq3qMd/Y078wGdw8R/PTv2iPYsw1EdS/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "B1wUMyamoTyUMO5y3sGMPwBk3Ezj8Jg/5DwM0x/AoT8cBl8uK0OnPwwPBn64Dq4/bHDP7Adosj8v9IgxccO1PxkRezCLHbk/5qJC1nhwvD9AeeDO7qG/Pz3iOYHdO8E/iM551URLwj941iQ5PhPCP7UbZT39isE/3z/sOn66vD8NoJvRLDWxP0b4gAiZZ4A/aJV5ZLfelb/pHP9dYpiOvx3LC5dFJ40/+XwP6y64sT/aDug3tknBP5l1cjIMysk/EMdprulB0T9liIVuuafVPxhQfcaYGNo/AFJlTweK3j9VLiCNVIDhP8I1RYj5vOM/QeoMEhD95T8QnfF4rzroP8YhcWHVdeo/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j9z6wT0eFvoP1koUnDSQuY/7tNbSnkl5D/WF1PCKQXiPz85IGwLx98/iPOoPBmD2z/LdUtnmz/XP1B9q26K99I/Hq5UyazFzT+gvCse9ZzEP64GLit5dro/hkhhI8jzoT/0bUgmN7+EP0BVfaSrBpA/OmNtTXNEgT9j0PQdzJKWP+4lSc69Sak/xRzhwF8cpz/PVVNxbnWlP6/P6OV0Pp8/B9xGPcssj79w+I/lYjK1v1bzaqI8DcO/GC5vGAN9zL+249zV8xTTv1QGuiqUkde/NGw92lr+279kLQeuwunfv5fa/ssspuG/fHBIJHo+47/sxsw+18nkv9/Mb4qTTea/qPSXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G078Ui+4G6W/Kv98F6GQ+3sW/sDInN2pNwb/H242sEoi5v8TQzdTKirC/M/3hJ72knr/ERzC5SztxP+LCAVlFRaM/fRSzLsFBsD9Wp8pngvO2P1baaF4/7bw/akM8HVKGwD9nptgNZB/DPztb5IBLgcI/ZO7muKahvD8XCk5mlnCxPy7OWRQKWoQ/7rJB/3sIiL/WjDmxHOecv2ZVuTwIhIa/9Ci4PY1RWb+PlNYC7+l+v8a7J7eqnHy/ujWKz4vvj79uBijHyz+dv/YP0KD41am/pHaYsYsMs7864Psm4VDAv4tBn8WfHci/pg5ENjD7z7+0mLV1su/Tv49vLsYg5Ne/XVp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6b+n7gNWyUXjv/kACiXJQuG/LbXhjYiC3r9D2Da+tYPav4n3uLQBjNa/KyTSM46h0r9Zx23udKXNvyogFYvQpca/vKXQlDKswL/jsn9+BWC1v9bvlghV0qO/8P1Shfy8ZT8W6Y5AQZiRP7pwzNOGGXq/kEfAcR+ccj9IOMhaTtKSPzoNK5z3zak/Ln+uY5Rmoz9s8ma2kzOXP4mKYMuWgpU/ZkjCiCtVpT/o23ZnSoq3PwqomZS8YcI/8Ni0UpoPyT+Uifzq2dDPP0g4ZXb0ZdM/3x+6IToD1z8McI3uB6LaP27Egn8eRd4/NM5lIqj14D/CLSRmrsniP+gM9tsvnuQ/qPSXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "X1p1BCPP4j+wfu4tNkHgP2yIuddP1t4/pE18YQAo3T+PK12tIHfbP1/nlMDGw9k/TJE50EIN2D++ekQbhU3WP4ulseCaXtQ/vVz2BbPA0T8Jr2ZfDTDOP3A4CQmq0Mg/RhznCvDhwz/JH/0EKQPDP/Tnc8u5TMI/Xi6/D180vT/lp96k/KmxP2IH7R/LjZI/+mA28jlCgL+QzTJlXi6Gvw0Ht1aXFI6/eL5wsEmmpD/BHFSg3ui2P3vTxjWaScE/M0P21xTzxj+jiVE+AnzMP4WLVKiy0tA/1hpgkpo70z81TUBY4Z7VP4wfGKTa/9c/SPzT6GNe2j8MEkilPbncP/FLBpL0Ed8/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G07+C+T0egy3Tv7YJTAUXlNK/6hla7Kr60b8eKmjTPmHRv1I6drrSx9C/hkqEoWYu0L90tSQR9SnPv9vVQN8c982/Q/ZcrUTEzL+rFnl7bJHLvxM3lUmUXsq/elexF7wryb/id83l4/jHv0qY6bMLxsa/srgFgjOTxb8a2SFQW2DEv4L5PR6DLcO/6hla7Kr6wb9SOna60sfAv3O1JBH1Kb+/QvZcrUTEvL8SN5VJlF66v+F3zeXj+Le/sbgFgjOTtb+B+T0egy2zv1A6drrSx7C/QPZcrUTErL/hd83l4/inv4H5PR6DLaO/QPZcrUTEnL99+T0egy2Tv3f5PR6DLYO/B1wUMyamkTw=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j/hI8ZvNHvuP8LyR8xah+4/o8HJKIGT7j+EkEuFp5/uP2VfzeHNq+4/Ri5PPvS37j8n/dCaGsTuPwjMUvdA0O4/6ZrUU2fc7j/KaVawjejuP6s42Ay09O4/jAdaadoA7z9t1tvFAA3vP06lXSInGe8/L3Tffk0l7z8QQ2HbczHvP/ER4zeaPe8/0uBklMBJ7z+zr+bw5lXvP5R+aE0NYu8/dU3qqTNu7z9VHGwGWnrvPzbr7WKAhu8/F7pvv6aS7z/4iPEbzZ7vP9lXc3jzqu8/uib11Bm37z+b9XYxQMPvP3zE+I1mz+8/XZN66ozb7z8+YvxGs+fvPx8xfqPZ8+8/AAAAAAAA8D8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r8U/SDDtfvgv2JDY6ijxt6/JLVzYd/X27/FHFTOMvnYv6M4rChBUta/Q9UX+LW0079WRpjhNEvRv+2hu2Pgzs2/kz54ON5Kyb/nptr5k8/Ev57nAGglisC/0PSlynmruL/EcXPPg6ewvw/W/GQUo6C/Ouyx3eT9Qb+KZqtVxG6LPwjzCeytOaC/5aDXMrHltb9r81wkbvHBv/mIv9Jb1si/p2XKM1a+z7/HKUqyMjvTv/1HjZhik9a/FiSoHfPg2b/BicKM1ivdv0I9x5HOM+C/kmIuCvjP4b/gaFa161fjv4ppJTBT2+S/+9eoKRA/5r+e8SW+r5nnv15s+v0ux+i/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T+2TR6iBujoPxroggls2+c/BqunNXaj5j9P/UOSh2DlP5cmGQuSAOQ/E9e/y/+a4j8GRkAYXx7hP8Nu0xutPN8/c11MXREh3D/i70yWhP/YPx5BAWl/ydU/KMVEllSQ0j+QuHbfU5vOP/72PGYCO8g/AwktuDTRwT/lOVKVG2K3P1qj12wBjaw/cIWP0j0Lsj/74tr/i2a4PwLIO0IxJr8/LPTtQMb3wj9oV4lUArjGP090ZXSbgco/z0AEbIBozj8h8NNCvijRPxm90ca9M9M/0pF8oPFB1T/aHixHfIrXPxj4Ho0v3Nk/Y0a634153D8EPrt2OSffPyWQPvvgE+E/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z/iVK5NhxHRPwGYX//+mM8/S5+6eVYFzT+Wr8OrSm3KP/++PKr608c/p9xz+KCGxT8Ha2VPgT3DP9rYWmPjEME/LituIyfWvT+BBJVVF7e5P8fi5WFrdLU/QyxtJjWlrz9EFIgyLQ6bPwQElJK+qIA/wp9v7qJ+nD+aDpW2nOOhPypY+2C0Y6M/7GG0bxKEpT940jnKJPixP15298WHMsE/CCyP2uJ/yT8wpqxLUb7QP65plq+bjdQ/Dx8vRCF52D9P8LVxf3DcP6o1fm5eNeA/BfDW5MIx4j8hEFrFYCzkP6LkDr2PJ+Y/3dZ/MhUj6D8HO4qVtx7qPzgVyh1fGuw/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j82GwAq3cXqPxKk/mEx7ug/5u0Jv8AW5z9YcQrQgD/lP06ulQqwaOM/XXIuSYSL4T9UMS/ac1nfP+niq+8ymNs/pgeeWjHX1z9Zfqor5RXUP39d2zZYTNA/FAYdMkwEyT+Q6Mn4JPfCP4Eb4Fw3ccA/rV1HDIcIvj9zdzc0rY+xP6BrAtxqxYM/cBSiZhpZmr+LxDiUYmiiv3n/OzvXTZ6/s3726b7nlb8seX14HAmGv1AL6j9cc3W/TAAbvKt0br8wrDJrlG1lv6owZwsBqFi/ngTwcR71Nb8OED2Os/lEP45zD3AVb0Q/Rrmll3wPQz8dVi33QC5BPyh+NdScsz0/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6T86H4P1vznlP+P+uTe+peM/8HVKIpIO4j87n/FhtXLgPxvbaAk8n90/IVNthUlB2j9LfN8Yh73WP87xe0lx5dI/gQUL8jkjzT+JVezh/HHFPyjSF1YRl7k/i3F+WoVInD9oIWiKXd59v9CNcY9orH2/Ypmm64popD+L3ua9mRWdP9oPfI+JFJc/yiiWsfY8mz/DkeiMyVGqP5Ad08WfQow/DMNPcycvkL8Zsam63vuyv/4nHcIgfsK/zWIrkicizL+qp+UNqK7RvwlBG3G2UdW/IvNK/l1K2b/M8qGHZ3fdvzoy4Tfy3uC/FfDzX00J47+M3pcbPjjlv1x2w3XRaue//1REEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "YFp1BCPP4r+ScC2kzWfYv5nABXA5itS/bHeRFY2t0L/S3fTL/abJv/OKuv7h+8G/XE9dIZ/BtL9FRtFaZlOXv0XP7/EEnZ4/Lnpdchwfsj9JTvE0cP+1PytncmAak70/D+eya5XKwT/MlD3FkQ3BPyeJiwgmh78/MkIoG5jGvT/H2TvVkUSsP8+IEHUCD4E/Sq9vIRLkor+/1resAcSZv+GjFGmKlJS/EAlRcYZikL8VNHZ82XZ+vwnOc3fgpYW/hUs2dijdkr+al4uyh3qSv6ZXQ+ex2XY/sMpfKhezoT/izXCHwMSwP9nsNH6z7bg/b20NT3KdwD+ZGDcZmc/EP0tU1OaBCMk/UekvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AFVEEw5v7r99Ukv721Hrv4rMWFjAPem/w934SoMp57/SSPKb3xnlv5KSmA5XEeO/rNzdUpsK4b/WFjVvpAnevzRxVooyAtq/vLYo0+YE1r9MfCIqgizSv8Qaobuo+My/foLJ6g4Uxr8/Jv7Ge1e/vwRVLBJASbO/+1eB+SLDl79zKPzcHvSWP206QMR2gKM/MblJgs3Roz/MhBCDSdmVPyRW/LiAAZG/nbPdIWVyq7+mAnC+yay2v/OT0wrbsL6/vCO+Z3Lnwr/nxPhJ8qzFv7DhrFmeOci/u+msrcdvyr9C3wpZZILMv8++iQoFhs6/l2MUQa5F0L/f73dXnEnRv8DKmvEIT9K/UukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "TekvN+/G07+yzRV2e9bSv4EKSivUC9K/ZikWp/Q/0b8ZTs6/mkfQv3KlpsDPL86/MtH766LBy7+D27PpAD3JvyHP0bQxjsa/xXh8F1C2w78aN3OClEHAv+RX7+J3b7i/8J17Rsktrb9HFVI/T4SMv9fTiC6WyaA/DI/vqrQysz8JONoikFGzP2skkVkrwpE/nN4EIR4Fnr8Sc5pcmYSwv+J5Efa3/r6/J4cnDNcZx786RQJwmLLOv+XTF5BpGNO/KnXnXi7y1r/gdUC3ft3av15AmtN33d6/iEPiC5Rw4b/3L5segnrjv3zTop1thuW/na2/G9KS579KeykAqZ3pv0u1TQZKqOu//1REEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "p/SXm3fj6b9rGm8kQXfiv+fpmuqHU+C/XMgOnHVn3L/kavSpqzTYvyHaLX1yG9S/PgH4Z+E/0L8CuC+jf6XJv6sqHyWNRMO/VD32cuHlub+igBDB8xurv2Soqc7nDYO/vOWOk/NynD+A5WM7YDhDP/A3DjmzKGk/1G4/kf09ez+7nQ0jwNKiP240n74zh5k/TvKcNjXinz/JNJHgqQusP+eT/j6y4JA/3CbDWIAglj+EIfJQVtewPwQSd64zjr4/FhEPWMzaxj+dGAGiAxnPP6C4bcOK1NM/bk7tZNsm2D927RXEgIHcP3hZj6yaceA/oySAa12l4j9F0HxQrtvkP+bN9DmtFOc//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "X1p1BCPP4j/TugevnmrgPzHnUFrEPN8/9606clGk3T+HixG78wvcPzdber2Rc9o/f7Uj3GDQ2D+c9FRFQd3WP2E6pZEGbdQ/WBKtBx250T8hHPUBbgrOP7RZZjRAgcg/WaFpHyJiwz+qoC09Wo3EP9AAcgHDYL4/TJJbHBIvuj/+AejUqTmsPwAqfW8BbGg/6Eqx2jvFpL8p9JFoL2OZvxr2zoWPTIu/2rJuYpNnm7+J6eHty/6ovx/xeuv+CLK/u01uP+oAuL/s+fAw4aS9v0H5ArExQMG/974kPBWtw7+Jyl7bTBjGv24gwIRVhMi/3i6xOiTxyr/4sUfXhF7Nv0RhtY1rzM+/VOkvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j+ESEWoEE3oP2JSWfpOMOY/R2RvQsMW5D8qLynH0f7hP3Uu9wX/zN8/+Go49viX2z/wrNd191nXP23z8Eh4GdM/MT4eat2OzT++cEpE1w3FP+TKNnfF3ro/DZblZz4wpT9AGjZ8mP55v4DuRKqQl3G//CWLAmGRmz9Bn1fNT0+UP3L7dFaiT6c/GHCfYhdloT8YmQOiGx2mP/I3P143Opo/bOQTHo7vgb88eSSnRTu1v8QXc5P6hcO/69rhlLkUzb+vuWyz9zXTv1sR3ebnVte/i5YgQU2b27+B8mgasOTfv/h8III/H+K/XBcdeFhk5L/Ig+qKn4Xmv8PDZYuwqei/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G078zsL4wOPLKvxP8u824yca/VcFi2Iyrwr/Efm4RlSS9v0Kxhtd9/rS/aTU6uS7dqb/HalmGUPqTv2Dk/+q6p4w/tDi07KqLqj8GSot464W3P46PNV0Jmbs/2CKaAxq6wD++ZqOQPiXCP5Iszpe2hsI/Cki4h9gJuz96Eo7oQI2zP7iAvvTuvHU/BgxGQgvEob8NxhxgNdmgv86O2dHl/5W/Ypa98+agf79seFPV5fVzv+uU3AYWM3y/Zq1WQl4zjr+scSRBc4+bvwrCDtXe76q/ye+S1AXes7/K6uSMCza6vzYbMuMMhsC/4E02Of48xL8jE3RCIfPHv4L1rYHsnsu/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G07+hbRD815zRv3bCuFmuftC/sBUSkIm/zr+4fA5GWXzMv8JGqi7WMMq/qlL8hfXXx7+FItebrA7Fv7tTuW6+WcG/ukJeWIjjur/4Zl5tJCKyv0UhOAYsF6G/XOnc1oLQeT/hji39F7yJPxDUeStJkIE/SrAlQvrZlT8YvNM8zpSeP9XO+62sPKY/t+WgLVCYpj/m9qrW4nSiP6ZwUs5pAqc/yho+lUDcuj+ceqmiK2bFP+NynB9SYM0/iUwORiSm0j/kyq6/gpnWP3Z4uTTvito/4VF9GjqS3j+aCYpIXlbhP3DbbwjDYuM/URsuWHhu5T/IzEXArHnnP6yDwSzKhOk//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j/3AH3KH1XpP28Q3cD6W+c/MUz7oIhg5T8Um33jK2TjP3IfKrOYZ+E/AWiC6fnW3j+ctH17mPTaPyPw3k3bSdc/eUJ9cnOk0z8kes74fCDQPz34W9s9x8k/Eq7VoFI5xD+qSPSyyobBPyZWKWuhXcI/X1LSmqnqvD9vtxBhrC6yP7BGf+dbI3k/VksX5YGNlb8ip4diQBefvwDxYktORwU/Xjsy3KWwoT9kgsk27pGwP5f1BBJYpbc/jSk+9+R+vj/HWIgr5o3CP2k62G05zsU/OXHmLcmFyD/FyhWNlcrKP/LJ56yVBM0/3RRIqzE2zz/DfwAHFbHQP73mBOLmxdE/T+kvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6T/sNDZNy9rkP2dVZhhM2eI/BI4WDMvX4D84O7HodKrdP7CTM4yUoNk/TPOBUQiV1T+LB0B01tPRP1VfTPwKpsw/vXtucm7NxT875t462PG+PwZ+zSepQbY/kN4QAC1Dqz+soVuwY+uQP54vzH14I4I/+hUXN6Tolz9Z4QVApT2lPxek3NVaf6c/3XzKIz4bpj+sNvYYw7akPxdr8X1Qp6c/nnX1+RJxsj87tvie0pq4P/mJiRZbSb4/yhdJ3pPawT+fBsXd5X7EPyZnXtpSHMc/sHrJFYaDyT99E7x8iYfLPxKDIDSFhM0/yEexiO91zz+HqAz42K3QP53ZvCRemtE/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "Xlp1BCPP4j8eC1CdhsPhP81uAJmrIeE/AXH+BJx94D9ciZ2E36ffP5vdlvT8QN4/ipg2vfW43D+FkEqBE6LaP/ahLVa0Hdg/BEY8j/th1T+8Zzrk+m3SP8YcID9AOs4/PiO6caUVyD+qH+dbqErCP1wmOiqeH8E/eqzEJ6bkvT+2D24a7E+zP6XkrP9ej4s/nOTPzTBQlr8UUasKKT2sv5CKXwxQobe/L6QQ7BKgwr8yt7rksqjJv7l1+flJdNC/D6QRM8Qs1L+3NQzV0fHXv5W0E7CVv9u/w7g+bn2U379GlgTxULvhvxkpaGmxreO/cTFz/lKg5b9KToXawJLnv7QcMqMghum/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z957rHJcJvRP/yVyTinstA/1B2OnGarzz/T3OVCnP7NP9M3iYaHVMw/mSonXVKqyj+rPvDgnMHIP+LzWjQZIMY/H0v/Ax+rwj+EjbXu/+29P6trIVZ+LrY/dkMp7wZRqz9tjAaaq66PP7Nn9kcjUYw/hqq1ncOZpD8Kq2gu2cCUP943lkUZjaw/XbWejPO3oT8MxDSLBMmjP0JrmW5Pgqk/ldYXujOxsz+kIbfUqoy6P8rbH1dKd8A/NlBTZFqjwz+lWWaTKJXGP94lnH8hUck/LQ7xhSM7yz9ebEYdbfbMP3Fz6u9yps4/yM7jvcco0D+nf61YOgDRP+gZZVbb3NE/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j/PvBuVyz7pPyoM2NupSuc/C0b5fk5b5T9acHvcN3HjP86sIbqljOE/YMJzKY5f3z8AbdIOvLPbP8HugBvtKtg/yX7cOtLh1D89guJG/fjRPyQUM0S+Jc4/Jz2zrhwGyD8OoueHJY7CP1vcNmbotMI/3/JGmOg+vT+uokxL3oOyP932EmH3jIM/d08rZcrnpb9u7LeNT5qrv5gsja/DhLa/uXWT83gWwr8p2qgIQuPIv/u+BME1uM+/+JYIxYpb07+eKGE+LgXXvw5xTmYK0tq/h/K1n8e43r+u+N+BfU7hv7W/pXJDQuO/0zawbk035b8A6vcZ1S3nvxIQcJ7OJOm/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "UOkvN+/G0z/iVK5NhxHRPwGYX//+mM8/S5+6eVYFzT+Wr8OrSm3KP/++PKr608c/p9xz+KCGxT8Ha2VPgT3DP9rYWmPjEME/LituIyfWvT+BBJVVF7e5P8fi5WFrdLU/QyxtJjWlrz9EFIgyLQ6bPwQElJK+qIA/wp9v7qJ+nD+aDpW2nOOhPypY+2C0Y6M/7GG0bxKEpT940jnKJPixP15298WHMsE/CCyP2uJ/yT8wpqxLUb7QP65plq+bjdQ/Dx8vRCF52D9P8LVxf3DcP6o1fm5eNeA/BfDW5MIx4j8hEFrFYCzkP6LkDr2PJ+Y/3dZ/MhUj6D8HO4qVtx7qPzgVyh1fGuw/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "/1REEw5v7j82GwAq3cXqPxKk/mEx7ug/5u0Jv8AW5z9YcQrQgD/lP06ulQqwaOM/XXIuSYSL4T9UMS/ac1nfP+niq+8ymNs/pgeeWjHX1z9Zfqor5RXUP39d2zZYTNA/FAYdMkwEyT+Q6Mn4JPfCP4Eb4Fw3ccA/rV1HDIcIvj9zdzc0rY+xP6BrAtxqxYM/cBSiZhpZmr+LxDiUYmiiv3n/OzvXTZ6/s3726b7nlb8seX14HAmGv1AL6j9cc3W/TAAbvKt0br8wrDJrlG1lv6owZwsBqFi/ngTwcR71Nb8OED2Os/lEP45zD3AVb0Q/Rrmll3wPQz8dVi33QC5BPyh+NdScsz0/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D9uuNhACL/tP1uf1P2Czes/k0/PiqfZ6T8WYn6tZ+PnP1jrPZ/s6+U/ekg0T8r04z8+nESgCP7hP3sWmaj1COA/Tc7Hd7Yu3D9TGLIeolzYP4QbmoYalNQ/Yy2DlP6x0D/VqcuCDGfJP0vRE9d9ScE/Qf2eSw+ysj9UZrYSSxCiP0OOdeZKRKY/IQ1Mr8Emqz92hi/BO225P389xuWc5sM/hLljG4qJyj8h0rmS52zQP9AmVmqietM/K3rLtKpw1j9hCzbyP2fZPwHpt5mvaNw/s1N80oN63z8TzK061k7hPyC+kWqd4eI/NFy8+9Z05D+cu641KhnmP2WmlJ3Gy+c/p/SXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAAArOawo1IWRPxP1Qo4uD50//1Tpn8Udoz/OSvOzCGGmPxBAZhobh6k/HS6ulfuKrD91Jy06YWevP5nDBuUXEbE/d2skNjvCsj9zhq5Z5tG0P2K65tMd9rc/1t0mfY4BvD/eC0iXlZm+P77hjXtmeMA/4vo43h7Lvj9iuiSb8Ye0P16sYN+BY5M/qfrZag+nnr+GElJ/zJuwvwdeePgHbbm/t96JbmOVwb9kXuhfNp7Gv6exYVuVv8u/zHGLUiGD0L/V6T2DNRvTv/9A9mVYqtW/frRbrPYl2L9bE83L9Izav48zUsZV8dy/z2JrHepS37/I+c1wmMDgv+a0YZvywuG/YFp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j8GwHxwZv/oP5gx+bPA3eY/i0xBf+665D/2Qa8Jn5biP4tesSqccOA/LWAC31CQ3D8nvF+frzbYP3OzY2T9vdM/DILBNBZkzj/0SRqh0FPFPzgLiOf/kbk/xBWd6353pj9uduhbHJuLP0q9IB/k7Y0/ykIqyvqEkD/2F6nR812kP9+nU06UcaY/7qdnsE9Ooz8OpvBLL1OUPw+PC1zoY4s/IlyKTNRtlr8K2OP7REe3v0qyvFQP98S/b/C3q09Lzr99OL3p5J7TvxGbMCrG8te/ZTMBQ9NH3L+P/YoygVDgv7ol9MKvdOK/f/H3vw2N5L+zn2d4+Kvmv02oMRPjzOi/AFVEEw5v7r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "T+kvN+/G0z9Quc5b+gzNPyCTl+UO28k/rporzOuzxj8BboCUG5jDP1F3OFp3isA/U2cC/Pwhuz+pAXNryna1P6ESoprLIrQ/UFIst+Q0sz+DQaGtNIq2P1qgLd8QFr4/dBSay4NOwj/ye/eta9XAPzU11rzrJME/mT3dkopXwD+Q4B53+CqzPxbN1RZCdpY/mr9nhO/+kb9Ot0Huu+2dvy4g4E9jlHa/sNS8Z/0Ogb+7ZeraLXODv3qpJYF6Aoy/tr9ug1W/lb9fdPqUX2Wiv2ghp7AAZay/sK5/yB8ptb+WfrLSmx68v4M7IOZzlcG/MTroegMYxb9HLPTMRqPIv4/0vB5+aMy/TekvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTw3/3i+sh6RP3XIFjzD9Zw/S/4uI4isoj+PfE5qGvClPwMDEz87Yqg/bDuUDeVpqj+KZruIwFurPyBx97xTzqs/hQgTjgpArD8gsqHroqWsP77HxV5kIa0/ALaEQhJsrT8/uVWZfU2vP6ag2qU0K7E/ZSA592sbtD+Mm/lzZ1C3P2SvhyoazbQ/z7XL1ndmvD+gjJapKPPBP1IXtPzVs8U/JYfuzO1ryT9DHuZJwxbNP/M7JgHrYtA/HiMJ4ZY60j/eE0f1jhTUP2EANjje8dU/d2uetIDc1z/p7AQODuPZP5B6MCka9Ns/Bnotz9cZ3j8dyDbiQCvgP7oW3vcTX+E/Xlp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D8I98UBYyjuP7ozqoFqfew/vQDh66LN6j9X2SzCNxvpP0dWeRBmZ+c/rhTfhM2y5T9+TuS3Qv3jP32a3bRcR+I/enZqzViR4D/WdqRnOLbdP5AD8fT6SNo/LEjH5gjc1j+mlw6g63zTP7gvM0dvKdA/PddOYN/AyT8qxf/kyDHDP+tUlaeq2r0/1tetfyOWxD/TWd/4okTKP5z0WXa5888/kRnARTDX0j8Vh5sq0LrVP/PgCjqyndg/5+caZrKA2z+YOrGmQWLeP5OYWFjcoOA/i1FoWdcL4j9NO1WM+mzjP2blJU/syeQ/mvQxnbse5j84hJI9J2nnP2QC8bsyoOg/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8L+/KsGBK7vpvwcHA5WMhue/quViIUpS5b/uR+XVXh7jv+4bFn706+C/ZtVCC4Z33b8x7QjEHhvZv3QeqSvZxtS/GhDUsAJ60L8vSty/vGnIv8R1hO+Dub+/RJlXUwTVrL+kKjP8ACZ2P0AAuvcX+FS/XZ41UaUIiT+CBHRZTZOjP+Aee3blf6o/vNR7ZkMmoj8ONca7eMCjP/Qin/Cpzpw/agJwKLupmT/cpKord3WgP4eSs/UWeKQ/AyP1ds05qj/d82Bh71WxPwhHxMNIS7Y/lHrtYUpzvD9m9IwN2GbBPwB4wBxBoMQ/cruHhMTfxz8p184L2SPLPx1jfuKXa84/TOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "B1wUMyamoTwTxwYG+RxQv+P1ZseBcmA/HyQCJFvrdD9THOd6kiWBP75WxjxoM4g/VU32N5Hnjz9UgPTGH2OUP0Bwp/6tV58/rzZUsq/Tpj+N0RL2K7+uP79mtJSRDLQ/UwR+feIjuj8X08wmWiHBP+ZGsb9nPsE/aqe+BBejvj+wEaAK+sC0P9A3eYhfZ5I/NMoqZjYpir9suxpoooWivxaCY1UVjaS/3joI5HNCuL8mRnEKPzrEvwugUpXfOsy/CFDXqzcF0r9DBBD7GeHVvyUKqhY/ytm/PybIFZmm3b/+4kSQWsngvyq+wa6RxuK/r1Vj/rvI5L9mC8DHxc3mv4YqjZUk1ui/AFVEEw5v7r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTyh1londV+mvx7IyiXsBrG/lBz7zAumtb/XTqOXAWK2v/A+TPWh9ba/6weYQYgjt78Vhurkv6K2v9Hf3pcxarS/R8tSZRSYsb/dlKxzEc+qv9XRwFKWbaK/EE/UrVmYlL98i8epsrBhvx7HFLLRuHs/PIat3TqYkz9eT64SYPCdP6K4h49E6aQ/ETcU282tsD+BCyCxNFu2P9x7hZ1B1rw/FZtpOiH9wT8dJHcqF5/FPwAlRddcWsk/vfD4V2EczT96jrTRuCbQP7ztyzoKntE/Aq0geXvB0j9yN5eo88PTP7XpHDpfstQ/8HdCK8eY1T8aZfXi14bVPyqp+OHTM9U/UOkvN+/G0z8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D/1xr/BIuztPwYb4B84Hew/JSKtUvZE6j++vX6ifFLoPyIUoNzTXuY/Dszz0qdp5D+2/VaVbXTiP3MQVOMhguA/dxo52OUj3T85qUfo0FLZPxVW+0usetU/IXGxwoKX0T9B6/fnwn7LP6T3LR0x+MM/DD5W1j8uvT/dSOuJb2ywPwfOyPr2j5Q/+684vSYesz8H+4o2G77APzCLhM/l18c/WNd+px/czj+MQ+hlze/SP52KFio4atY/Q6OOgRTh2T9FAKq2WnrdP0tAWsH4kOA/878CTzRy4j9Ww44LXVfkPzxjfQPMPeY/hA65+hQk6D/vbMZ9YhTqP3mwlBszBuw//1REEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j/rJEM7s3vsP2oocyAkGOs/Ah3O77Kw6T+ar37fbDfoPzQ14WrscOY/yXjQXAKa5D+kmEW4mrbiP5lTLNFixOA/9vuVvS5x3T8mgHHz/y3ZP3qeknKZrNQ/pWilxHUu0D/nkj0fj3DHP300wBsCjr0/9PHFiJaqqj8r3FL85pWjPzH6NRU4o6Y/A1Xcx/UsqT8mERY21P27P+C0GMRH9cY/JFOSBiX8zj8DMObC76DTP2wo7E/Xvdc/vx3ohSCU2z/WmhBcBK7fP0HFJtE1yeE/5wCKgZus4z8h5DrnrojlP4Rsqd/5Xec/n8sxouII6T/goVuv+qzqPxMP6FWQOOw//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G079Sxid6uTvLvw4+bxZGX8S/qMC2Vh8Su7/xYU/vXAWsv5yvPyW4wIy/sOIvyfefmj8f80GiI2SwP/5lur0ehLk/67V/S0S3wD+IdHo4ncvDP7IiF8f3csU/VKdsyeyAxT+zHLsiIODDP9F7xVnYgcI/5EoAdtHsvz+fEB1lb32yP3RuqI/KZJA/BPCEcxcgoL/aTZ3nlLOrv4v5XSVqmrG/ZWaVa6ATtL8R054Thv62v44RNhCb9ra/PGKXdUR4tb/qa1kKnkmwvx2sk9zBX56/NN4S0TF3ej9q567amkymP6eDqHtP1LQ/wmFYfzMgwD8H3qqF43DGPyy6nHuVzsw/T+kvN+/G0z8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTzHo7yk+suPv/t6AHSoDZa/Mytdm7wcnL8DQX9s8Ayhv4SAqn1E5KO/JAtkNdJPpL9aaSxT7kSkvwTIzD+UR6S/ZHPUY4FEpL9WOkivOiikv1Z555Jtu6O/io/808cZoL95xNVxF1KMv/76QVWcgWU/YZTnLKR7lD8KTbFr5HKhP2vSs+D3SaU/oVaKZLjBij8m0QZsezKrv0jQEmOZOL2//lA1clz7xr/M8TW/Q0jPv2ymlTPFztO/iQB8yAD417/YjqNwhiLcv8PfN5/DIuC/eGy/o6Eu4r/l+yjQVDXkv+TdbfLmNea/tG+aLYo06L9BgnEiCjPqvx/WjpgRMey/AAAAAAAA8L8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D+40/u93RDsP5+lUqA0Euo/LUebC4cT6D92N3eoGBXmPwCGj+0eF+Q/FsYt8Fcb4j+nxvGurBrgP1XB7wfVN9w/p6c+pqc+2D+zzQXPQUrUP67uUvsfWtA/HxCho2/byD8KNXyAEd/BPyA01CAQLcI/TgQY/gwJvj+EYiRyBxqyPwe7LfRuU4Y/QfTNokDOlL+yr4CIMh+CvxS+iKxqtZE/VTA4TjnEoT+VLyyCzc2nP6kIUOL43ao/8usWO+ZLrT894Gdo6yquPyHXAXJ6aK4/jp+mSNberT/DCYof3jOtPygQkAJ8Hqk/0K9n+vSXpD9OXFxJGwmgP57kgEwl55Y/B1wUMyamoTw=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D/4B6RCgv/oP6PA061KpOY/PuP9oH9S5D/dHURICf/hPzYPRtWTe98/vORAaGMj2z9c75IwYfbWPxgaFlXX8dI/6cvWKS5hzT+qIVs+c33EP/J69ujrjrc/WE91ca8WoD+UrE1K0vmAP39CYPeRxX8/Rg5oOWKHmj8cyk0OEb+hP6prGUBdwZk/8BESw7bdmT/8515TQ2CmP2vHuT1TY50/DKJkBqO4fb9iWEycpCCyv5xbfQ6t1MG/D+wh00X5yL8pO6EZMRjQv9PUQkEJbNO/8N8jMnPO1r//fPcpAUPav1TPSbInwt2/3gjf6Tmk4L+Dp737jmriv8z7kypANeS/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAAAkb9fyCNF2P6CLudPtX4M/NkmzpweDjD9YPZdpxTmTP4OMjmR3iZk/RzHBMp4JnT8pk30LTROhP1NBcLPKpak/Bmr+lNwgsD85HahdLiy2PxC3YJa/Pr0/SME4ZboYwz9gbG42MpPEPwR/4F8KFb8/eHeK7bVYuT9KTNspcLKtP/BLxqcxB5E/2gqGvxZrnr81C+YCexqjvxA7hX0ix2A/GmZU6/f8c791aOwtg6l1v3rehmVhInS/qItLkRRrlT80K67F+DetP/7n/nt4Vro/0ChDEZRLwz90Sc7VqJjJPwcbywXDANA/Di+RzFw+0z/RF/W3WoLWP0if94o6zNk/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "B1wUMyamkTzQFoMDfOqkP8BGpiHbNas/H7xYgoS1sD9U82KuUL6zP8A4n1sFrLY/o2iWndt3uT+fz1gPgwW8PypvMRqPb7w/BoPbHyxLuz+TQ5mzQcq4P7Gfxvu3E7M/vmY6G5DQpT8sLjTfs1OLP0I6i9XLvnY/eMrSox3voD+GRukSs6qWP6IZt+yOU6k/Z5Yz4FkJoz8KU9lnzamlP06TgRg8WKk/clYIbwrGrT/vTvvkP4itPxJsC2sJAa4/rmDXFSKtrT9gUA0LqJisP2gugZ0uVqs/xjmXlYXfqD97zFzjdRWmP/HiEPRPMqM/ozewS+kzoD+E1mVex+WZPy+hXgxSaZM/CoqeTDl5qrw=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAA8D/WpadO4LTqP6M6R0rPuug/Dqp+28vA5j9y2bRQW8bkP+g7ysIAy+I/A+piA9PO4D8KBXPpz5jdP5T2Verzidk/xjqs9meU1T/A8mj/4NvRP058BJUEWs0/oNvH5ecaxz80FM0AvU/BP9mpyaXFbsI/UGnFHFtWvD85LKejUFOxPz79WT22dno/tb0+WJlrkb8+Ud9uQ+Kkv/9lgOdlB7a/Mmnk4Ljywb/MXvTf2RfKv4oGNV9DG9G/pHB1sP0s1b/45KYccjnZv+kIYSBmQt2/25v/QzKe4L8WxADpi5zivx9QSD6SneS/95fEdvqf5r8V9lGwoKrovxDLUmUzuuq/AAAAAAAA8L8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6T8PSksniPLnPxYRoQgwn+Y/HWvtqg9L5T+EP5rWT+zjP7WQF2C1h+I/iPKWOdMk4T+cY+IrAI3fPwGO3SwQxtw/VrybgbKK2T+VWBTAhbbVP2xKt0ACx9E/Ka8/Ci9yyz8GeTvPmwbDP30MzQ4FpLU/nmfmlj5roz+K5dSVe8GkP2qmlIvIxag/d2C37NGOrD9+9VF7JYy8P8+DUfeIrMY/8cXHqOUmzz8961C3sKXTP7TnM6MJwtc/6DWaBxOc2z92jCgMeAXfP5cMK9qTNOE/ElGORDHs4j+OPudhQKfkP4e8QiEwZeY/FFTGqeMn6D/UD8A42/XpP4Zlf8r4xOs//1REEw5v7j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "Xlp1BCPP4j82+zlbZ7rgP/lIpVxnqN4/j93op6nV2z+DbeB9BCDZP7iTUxLRdtY/289GzrjV0z+ShyfX3T3RP4G4yiR53M0/WrT3tvHRyj8duMVlfO3JP7J954Ssvsg/K7/tTcX9xj+qdMSvfILEP5NBAwR/5MI/FEbpIookvj9SI+Su4SOzP2Lpxek5PHs/er4HT2Wvnr/5azVP/lSsv3k0UlCjDbG/lBV3t7rCs78DHp+0sRG2vzgJ4FoGy7W/0ufZQW6stL9Il3yMBF+4v8aW23Yx676/nSrEAbbxwr+BUFqj1XXGvyvetJq1/sm/G5MO/rBazb8mrTIriTnQv4A8Mt19xNG/VOkvN+/G078=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "Xlp1BCPP4j/d5z7QkKXePwNBK1HP5ds/bBBm4Ykv2T+qsyMSXnbWP8fyl7qcsdM/2IDE53760D+0wv03+x3NP3GtHBX6tcg/I9cso7tPxD8wL4/Uv/W/P8zoQoAVdLY/kJ/jBwMrqz8Eu7dDjVySPw381iKWZpI/6ujOGq7ImT9ZBdiE8xehP8r2Tmq+i6I/FLAmSBHypT/EmttzpR+pPx3JIhE5ma8/cBmN5kcOuj9ci9WetmDCP7tbYFSHxMc/eDRgjpfZzT+pu2sYQQHSPyKUetDQJdU/lAvfaTBk2D+QoPtPgBLcPy51nwSOv98/nTdR6Iy14T+Aim9JH4vjP2uvSBkpYOU/p/SXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T/G+PNovKHmP1WblXHnN+U/pPYMCCLS4z8jq3rNi2ziPyyzRdL9CeE/qvnxKpg63z/Z+gHpFyXcP5YUJJHI7dg/rU/n5WOz1T9vlUWAZ3fSP+d3fB4bis4/SEH9R7ZayD8jD+qyqdTCP/6Fb0yNDcI/MhVvk4Llvz9xba2csciuP1qYWOlUfJQ/c2TURg9zmr8OvybGCfqrv0+rJG2SlLi//CBlNQwgw79kJRr8+HjJv2bOsB1Fmc+/CSv+K9eV0r8u7363MEDVv/mCHttxzde/jwwkbjwr2r99IUPgJOfbv4kb8ISyl92/0s5QX/M/3790kYXJV3Lgv7g1BYNyROG/YFp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "qPSXm3fj6T97VFKpivHmPxK93HzdXeU/ser0Q9bL4z/J74TLzDviP86/ebwIreA/uedA9nFD3j/DQHr0YizbPwekzyrlGtg/qGa5fMoP1T8rv5b/URHSP0ismWqopc0/DHCwvcmGxj9EroGm+dy8P0AIUCsTMao/NIM34TYroD9FzpHNPHuhP0iPgjgSBaM/HZef59LSqj8+zxg92Ha2P+z2sDu1IsM/kdAmOatzyj+IIVxMiIbQP5TDgPdxktM/i/b5wBuE1j/2Q6aHCW7ZP9gMuiBSXdw/HN7DMK1L3z+6POqdyRvhP3R4+/5vkOI/2BQlvKID5D+odP4CfnblP2UdA49K6eY/p/SXm3fj6T8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "Xlp1BCPP4j8y8JjoTsPgP47wfFSZRd8/mcWuxRsN3T9uUDAbDtraP+CWO/awptg/ErgN3eNx1j/rLolYIELUP9Pb+H5XHdI/0S3lR08S0D9LQ2lXvHzMP784QwSng8k/pKGGUc8Txz/iZ0iUvEzEP5ft031QE8I/TOcxVhqavz8LUg170umyP0XJu5MKiY8/LjGUFEBPmr+pJF4Evr2sv4A9OTSSY7O/iUAhVJIeub+ywU0+yZHAvxMPOtO4H8W/3j+jIxjWyb8MFHzZy7XOv0dHT9xV09G/3gelBO1M1L/Dwg9vWMbWv0zRJzgRRNm/Zkeb/o3A279pGOnKMTrev7VUQIdRVuC/YFp1BCPP4r8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j+ntds/vCDoP/7x6yOkCOY/od3oyXbz4z8Cmd/2eeDhPyfqk5oknt8/zO2s139+2z+sEhYlw17XP7lLZpQQPtM/fJCAIo3tzT9QJYlbvljFP1lgslQfKro/QCRMJ/8Rpj8Sa24RzG+OPzhW2qRPwms/KpLMX0Uxnj/65mC8CwqWP2UjCl6Px6w/3Hy78XHvqD+MsB5Ze8ekP0Upug0ftqA/0kXkdDsAf7+3U9DHk+ukv7DNYFfDeLK/KMv4qRNQur9et+Fz6ZDAv5rj54DzLcO/irHyzyS1xb8Eju7rWzHIvy1X/fOeocq/20T4IoIHzb9F12dOH2XPv07ctx0P3tC/TukvN+/G078=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G079hkT246dXJv8sBkG8IbcW/NQeB7i4Gwb+07zBv9kS5vxejMe+LiLC/A4XTC4Z9n7/wIjd3k7JjPxaz0kgtl6E/+072kzqbsD8B0OJc2yS4P8nmsDpR2L4/91r/2piCwj+lrGvm/ObCP8hgcwlwncI/e+cjMlZkuz/F6ZVb1a+wP8sxZfq4K4U/lkDttWFQi78tW13r6TGiv5dCwVGK16G/adIm3LyJnb8I8pTC7yOSP+KrsX8V9bQ/mWHGpSAAwz/ZavZ8t87LP34PuZK1eNI/M5dxepcN1z/6grM0x6bbP7U1ZYQcIeA/UD9xkEhv4j9g573prb3kP+3c8B8xDOc/AFVEEw5v7j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "/1REEw5v7j9N1/H0DRboP/rh0a5j9OU/XGzOHtDS4z+OYxg8R7LhP7Fm6/LqJ98/xN8bIgzx2j9zt7zb4sTWPy77uZJmu9I/9dr431vpzT/6ynWv8O7FP94mOTzi17s/Cz0sZPuVqD8QjgLgSVBav8DcwyOCAU2/eJSuacdOiz9baUDTj1KWP9LeqGVPJqE/BDnrVQwflj8SWyFn3xuTP8/yWVm0JJU/1rayH82gi7+uvT3k39q0v9gTfZ+QfsK/M+sz9qxRyb+xeGN9HjjQv/6pWRxrh9O/dySQTmzk1r9vSSq1Z1Tav5YKpB5Uz92/Y0hF6dOo4L/Z37fiwmziv17K9eZVOOS/p/SXm3fj6b8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "VOkvN+/G0792GJ+sDf7Iv9YCQsFEM8S/hNWAgHLYvr80vKgQyle1vywCM4++z6e/jth8KXpnhL+eOobehKyaP6ekVVsjsKg/EHOHyNU8sT+DxhUAGe+2P6eLXiIMDL4/jffzD5Tdwj/VwAcw6EXBPwUl8NHWlb4/S9DXAbEKuT+Fw3Y4t2OtP4CQe/qtu3C/YqSGDDh/m7/I9R5Ri4SYv5dSgpaIHZq/nqGilucakL+97Vtx0x6Lv7Q3dXvdl4u/roQ3J+H3cj/6xWCah4WkP1wo3HkbALY/N2554vxBwT8jZx/k1LzHPyM18ve4WM4/ARjm7HaE0j+ULNJvUuPVPx9GKYzua9k/X1p1BCPP4j8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "AAAAAAAA8D/W2Xl5qxPpP2C8CZY3t+Y/bK+I01Za5D8qwjcSoBDiPzQEGnddrt8/xtHMKGEx2z8Y6OqeUvXWP65iKgLg9NI/kZhasgo4zT9qpMWg1+zEP/id1ewMQbk/1qcd6OwfpD/LKwctZAuaPyDbo1P+4Do/fjIcB4dcnj9fMbLddkSiP5VXn1xM5J0/84w/WFkOnD/Iayn+78ucPzOvRIym6Jw/pPJrmTYHg79khurJKn+xv07cwi5Ymr2/3tFOrJzzxL8fTBp6os7Kv/TP4+m93s+/zg5RGitr0r83D3EiIuXUv2lwoQFzXNe/WOQTe+3Q2b968mXtyULcv/XxZiVIst6/XVp1BCPP4r8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AAAAAAAAAADcSQHVmzZxP1prUtrrtIA/MI6QqtMrhz+7ynr9z1aPP849Z5Xlt5E/aCv0JKjrlz8iTAo1gpqdP4ZRUsHfraU/Bsz0nZnVrz8Gx5TVCAO3P/du5F8dR78/0iR67frFwj/wx0bjAfLAP2QZVlXC+L8/F/aa70pUwD9PJ5rgCfyvP0qMmyG14ZQ/RlPw7luilL/RV1mHpvigv1jERvxgS4+/+HK6SJE0j79o1MSpahBtv2wfCxzX6KY/fK4n3uVguT9Hz/y7TxHEPxHXNVelCMw/7wxQV/YK0j/k+XQ+XRzWPws6JmEVNdo/KYsOJ5pS3j/jTdTotjnhP7uQlNtIS+M/qPSXm3fj6T8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "TukvN+/G078ayj1JTXzNv+vjVOQVC8q/C5rR00qZxr/V0J2P/CjDvwhsCmDbeL+/1DiiowKsuL9zuAPKpPCxv02LX13dxaa/ex4TTn/0k79U6XPon9tyPzss0BdEVps/Xi44cfVWoj8imV/mzk2cP6DK677UK5U/WkCLwQdliz+IcGCg4kqdP5WvEisic6s/GX5wTp2krj+Eq0D/gZKmP0v2SZ8VdqY/PnCldKfuqj/awi0tcISyP5+7IlqS17k/JiA3vsnswD/fXawIuBHFP1lBOUZq/ck/xcbpiWM4zz9nggFFH0TSP1bjIbCm8tQ/UGX6e6+k1z+V6IjneFjaP70oJaAXDN0/XFp1BCPP4j8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "AFVEEw5v7j8qMxnUswHpP70X9gnDJec/HFzIm2pL5T8+5xWzEnPjPz/dbEmAneE/LWCg/1mX3z9rAErpBPnbP7QRKUyBYdg/tsZbQBrR1D/RC3Yj+FjRPxqKKjBUE8w/gpsOyeLxxT/rdah3CSPBP/jEH6tpj8I/XqbI/N6dwD+SMkimxQu2PzlTmv5WcZY/C+BkHJtymr+/J7ACSUWpvywF//nOCrS/ktmaS1F4wL9LJB6NIuvGv86g2/6Gcs2/Xwt/DjcK0r8rC8C6r1bVvyVW4XFcati/eopKuFZk279R8gsGrl3ev6a6y1Pdr+C/BIDlajc04r8+x46Md7vjv3PuFBusReW/qfSXm3fj6b8=", - "dtype": "f8" - }, - "yaxis": "y" - }, - { - "hoverinfo": "skip", - "line": { - "color": "rgba(255,255,255,0.3)", - "width": 0.1 - }, - "mode": "lines", - "showlegend": false, - "type": "scatter", - "x": { - "bdata": "XVp1BCPP4r/1sUntqO3dv5zwnYkHK9u/r+Kqjadn2L/BN19NS6TVv1shPvv84NK/Ae4ZcZAc0L+ewo8kDpfKv/ALD9S4kMS/kpvMTHjuvL9ajrs2WQCxv6Z1abrJ1Je/jfZ61IfwkT8eTqcvF4FwP6qfxHj/KWs/N9ReOm55oD9uJcJ2ndOlP75LZkK87Zw/sYvSXSgDnD/a8K5lq+uZP6QukIfWZJs/ToMIaDusrT/39Ag2Vqm+P0hWpeb4ksc/ZQ7oAhjxzz+pY45Q4DXUP6TXyTk4jNg/wA8FA7fn3D+wP9os153gP0Q1banIxOI/hwytSsTp5D/WidwFag3nP4WGFoIoMOk/AAAAAAAA8D8=", - "dtype": "f8" - }, - "xaxis": "x", - "y": { - "bdata": "qPSXm3fj6T/YFtR9FzLlP8WZe+Nkk+M/2oeJgVH24T/zV1MV9FrgP+DjDX3jg90/TSmDeE5Y2j8GXYyeRj/XP1LLemc/g9Q/n7lJFKXT0T+S27vSXlnOP7XwuOXw6cg/RZ5dhd/Qwz/SaWy4+snCPyFzSkFo+78/saGDfkCavz+bjv3k2g2xP+x73qnBg4o//MgudB45mL+Sr7F5V/qdv/QUkkVOzYO/MYpTl/fzj7+ZiI/gv8SRvwRNgPSkQZS/RmAfveG0lr8SIw96kXmYv/pBKeBiJpm/Ku7AoZRKmL/BvTn54mSWv3SqaHzgC5S/tJGP1at5kb80+PLS9oqNv7lR7uXl9Ie/AAAAAAAAAAA=", - "dtype": "f8" - }, - "yaxis": "y" - } - ], - "layout": { - "legend": { - "x": 1.02, - "xanchor": "left", - "y": 1, - "yanchor": "top" - }, - "showlegend": true, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermap": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermap" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "xaxis": { - "anchor": "y", - "domain": [ - 0, - 1 - ] - }, - "yaxis": { - "anchor": "x", - "domain": [ - 0, - 1 - ] - } - } - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [], - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", @@ -5980,544 +106,18 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "a2ad21a9", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.microsoft.datawrangler.viewer.v0+json": { - "columns": [ - { - "name": "index", - "rawType": "int64", - "type": "integer" - }, - { - "name": "x", - "rawType": "float64", - "type": "float" - }, - { - "name": "y", - "rawType": "float64", - "type": "float" - }, - { - "name": "xend", - "rawType": "float64", - "type": "float" - }, - { - "name": "yend", - "rawType": "float64", - "type": "float" - } - ], - "ref": "890e3806-880d-4d7a-85dc-2b6bc7832d18", - "rows": [ - [ - "0", - "-0.30901699437494734", - "0.9510565162951536", - "0.9510565162951535", - "-0.3090169943749477" - ], - [ - "1", - "-0.30901699437494756", - "-0.9510565162951535", - "-1.0", - "1.2246467991473532e-16" - ], - [ - "2", - "-0.587785252292473", - "0.8090169943749475", - "-0.30901699437494734", - "0.9510565162951536" - ], - [ - "3", - "0.8090169943749473", - "-0.5877852522924734", - "-1.0", - "1.2246467991473532e-16" - ], - [ - "4", - "-1.0", - "1.2246467991473532e-16", - "0.5877852522924731", - "0.8090169943749475" - ], - [ - "5", - "-0.587785252292473", - "0.8090169943749475", - "0.8090169943749475", - "0.5877852522924731" - ], - [ - "6", - "0.9510565162951535", - "0.3090169943749474", - "-0.9510565162951536", - "-0.3090169943749473" - ], - [ - "7", - "6.123233995736766e-17", - "1.0", - "0.9510565162951535", - "0.3090169943749474" - ], - [ - "8", - "1.0", - "0.0", - "-0.9510565162951536", - "-0.3090169943749473" - ], - [ - "9", - "-0.9510565162951536", - "-0.3090169943749473", - "0.30901699437494723", - "-0.9510565162951536" - ], - [ - "10", - "-0.9510565162951535", - "0.3090169943749475", - "-1.8369701987210297e-16", - "-1.0" - ], - [ - "11", - "0.8090169943749473", - "-0.5877852522924734", - "-0.9510565162951536", - "-0.3090169943749473" - ], - [ - "12", - "0.9510565162951535", - "-0.3090169943749477", - "0.8090169943749475", - "0.5877852522924731" - ], - [ - "13", - "0.30901699437494745", - "0.9510565162951535", - "0.8090169943749473", - "-0.5877852522924734" - ], - [ - "14", - "-0.30901699437494734", - "0.9510565162951536", - "-0.8090169943749473", - "0.5877852522924732" - ], - [ - "15", - "-0.30901699437494734", - "0.9510565162951536", - "0.5877852522924729", - "-0.8090169943749476" - ], - [ - "16", - "0.5877852522924731", - "0.8090169943749475", - "-0.5877852522924732", - "-0.8090169943749473" - ], - [ - "17", - "0.5877852522924729", - "-0.8090169943749476", - "-0.8090169943749473", - "0.5877852522924732" - ], - [ - "18", - "0.9510565162951535", - "0.3090169943749474", - "0.9510565162951535", - "-0.3090169943749477" - ], - [ - "19", - "-0.30901699437494756", - "-0.9510565162951535", - "-0.30901699437494734", - "0.9510565162951536" - ], - [ - "20", - "-0.9510565162951536", - "-0.3090169943749473", - "-0.587785252292473", - "0.8090169943749475" - ], - [ - "21", - "-0.30901699437494756", - "-0.9510565162951535", - "0.8090169943749475", - "0.5877852522924731" - ], - [ - "22", - "-0.5877852522924732", - "-0.8090169943749473", - "0.30901699437494723", - "-0.9510565162951536" - ], - [ - "23", - "0.5877852522924731", - "0.8090169943749475", - "0.5877852522924729", - "-0.8090169943749476" - ], - [ - "24", - "-0.587785252292473", - "0.8090169943749475", - "0.5877852522924731", - "0.8090169943749475" - ], - [ - "25", - "0.9510565162951535", - "0.3090169943749474", - "6.123233995736766e-17", - "1.0" - ], - [ - "26", - "-0.9510565162951535", - "0.3090169943749475", - "0.5877852522924731", - "0.8090169943749475" - ], - [ - "27", - "0.5877852522924729", - "-0.8090169943749476", - "-0.9510565162951536", - "-0.3090169943749473" - ], - [ - "28", - "0.9510565162951535", - "0.3090169943749474", - "-0.9510565162951535", - "0.3090169943749475" - ], - [ - "29", - "0.5877852522924731", - "0.8090169943749475", - "-0.5877852522924732", - "-0.8090169943749473" - ], - [ - "30", - "-1.8369701987210297e-16", - "-1.0", - "-0.30901699437494756", - "-0.9510565162951535" - ], - [ - "31", - "-0.587785252292473", - "0.8090169943749475", - "-0.5877852522924732", - "-0.8090169943749473" - ], - [ - "32", - "-0.587785252292473", - "0.8090169943749475", - "-1.8369701987210297e-16", - "-1.0" - ], - [ - "33", - "-0.8090169943749475", - "-0.587785252292473", - "0.5877852522924729", - "-0.8090169943749476" - ], - [ - "34", - "-0.30901699437494756", - "-0.9510565162951535", - "-0.8090169943749475", - "-0.587785252292473" - ], - [ - "35", - "-0.8090169943749473", - "0.5877852522924732", - "-0.30901699437494756", - "-0.9510565162951535" - ], - [ - "36", - "-0.8090169943749475", - "-0.587785252292473", - "1.0", - "0.0" - ], - [ - "37", - "-0.30901699437494734", - "0.9510565162951536", - "-0.8090169943749473", - "0.5877852522924732" - ], - [ - "38", - "1.0", - "0.0", - "-0.9510565162951536", - "-0.3090169943749473" - ], - [ - "39", - "-0.587785252292473", - "0.8090169943749475", - "-1.0", - "1.2246467991473532e-16" - ], - [ - "40", - "0.8090169943749473", - "-0.5877852522924734", - "0.30901699437494723", - "-0.9510565162951536" - ], - [ - "41", - "-0.587785252292473", - "0.8090169943749475", - "0.8090169943749475", - "0.5877852522924731" - ], - [ - "42", - "0.8090169943749475", - "0.5877852522924731", - "1.0", - "0.0" - ], - [ - "43", - "0.30901699437494745", - "0.9510565162951535", - "-0.9510565162951535", - "0.3090169943749475" - ], - [ - "44", - "-0.30901699437494734", - "0.9510565162951536", - "-0.8090169943749473", - "0.5877852522924732" - ], - [ - "45", - "-0.30901699437494734", - "0.9510565162951536", - "-0.8090169943749473", - "0.5877852522924732" - ], - [ - "46", - "-0.587785252292473", - "0.8090169943749475", - "-0.9510565162951536", - "-0.3090169943749473" - ], - [ - "47", - "0.9510565162951535", - "0.3090169943749474", - "1.0", - "0.0" - ], - [ - "48", - "-1.8369701987210297e-16", - "-1.0", - "0.30901699437494745", - "0.9510565162951535" - ], - [ - "49", - "0.8090169943749475", - "0.5877852522924731", - "-0.9510565162951536", - "-0.3090169943749473" - ] - ], - "shape": { - "columns": 4, - "rows": 100 - } - }, - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
xyxendyend
0-0.3090179.510565e-010.951057-3.090170e-01
1-0.309017-9.510565e-01-1.0000001.224647e-16
2-0.5877858.090170e-01-0.3090179.510565e-01
30.809017-5.877853e-01-1.0000001.224647e-16
4-1.0000001.224647e-160.5877858.090170e-01
...............
950.951057-3.090170e-01-0.3090179.510565e-01
960.951057-3.090170e-01-0.8090175.877853e-01
971.0000000.000000e+00-0.5877858.090170e-01
98-0.3090179.510565e-010.587785-8.090170e-01
99-0.5877858.090170e-011.0000000.000000e+00
\n", - "

100 rows × 4 columns

\n", - "
" - ], - "text/plain": [ - " x y xend yend\n", - "0 -0.309017 9.510565e-01 0.951057 -3.090170e-01\n", - "1 -0.309017 -9.510565e-01 -1.000000 1.224647e-16\n", - "2 -0.587785 8.090170e-01 -0.309017 9.510565e-01\n", - "3 0.809017 -5.877853e-01 -1.000000 1.224647e-16\n", - "4 -1.000000 1.224647e-16 0.587785 8.090170e-01\n", - ".. ... ... ... ...\n", - "95 0.951057 -3.090170e-01 -0.309017 9.510565e-01\n", - "96 0.951057 -3.090170e-01 -0.809017 5.877853e-01\n", - "97 1.000000 0.000000e+00 -0.587785 8.090170e-01\n", - "98 -0.309017 9.510565e-01 0.587785 -8.090170e-01\n", - "99 -0.587785 8.090170e-01 1.000000 0.000000e+00\n", - "\n", - "[100 rows x 4 columns]" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "edges_df" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1c25fd13", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "env (3.12.10)", + "display_name": "venv (3.12.10)", "language": "python", "name": "python3" }, diff --git a/examples/Examples.ipynb b/examples/Examples.ipynb index ee48084..85eb0c5 100644 --- a/examples/Examples.ipynb +++ b/examples/Examples.ipynb @@ -2241,7 +2241,7 @@ ], "metadata": { "kernelspec": { - "display_name": "env (3.12.10)", + "display_name": "venv (3.12.10)", "language": "python", "name": "python3" }, diff --git a/examples/complex_plot.html b/examples/complex_plot.html index 2d0a713..93bb890 100644 --- a/examples/complex_plot.html +++ b/examples/complex_plot.html @@ -1,3888 +1,7 @@ -
-
+
+
\ No newline at end of file diff --git a/examples/coord_flip.html b/examples/coord_flip.html index 7706df1..310e6e1 100644 --- a/examples/coord_flip.html +++ b/examples/coord_flip.html @@ -1,3888 +1,7 @@ -
-
+
+
\ No newline at end of file diff --git a/examples/edgebundleexample/.DS_Store b/examples/edgebundleexample/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/examples/edgebundleexample/.DS_Store and /dev/null differ diff --git a/examples/four.ipynb b/examples/four.ipynb index 7e1b6c3..0e48290 100644 --- a/examples/four.ipynb +++ b/examples/four.ipynb @@ -363,7 +363,7 @@ ], "metadata": { "kernelspec": { - "display_name": "env (3.12.10)", + "display_name": "venv (3.12.10)", "language": "python", "name": "python3" }, diff --git a/examples/ggplotly_master_examples.ipynb b/examples/ggplotly_master_examples.ipynb index a7d777e..d4e1107 100644 --- a/examples/ggplotly_master_examples.ipynb +++ b/examples/ggplotly_master_examples.ipynb @@ -3396,7 +3396,7 @@ ], "metadata": { "kernelspec": { - "display_name": "env (3.12.10)", + "display_name": "venv (3.12.10)", "language": "python", "name": "python3" }, diff --git a/examples/ggplotly_showcase.py b/examples/ggplotly_showcase.py index b22ba40..0e636c3 100644 --- a/examples/ggplotly_showcase.py +++ b/examples/ggplotly_showcase.py @@ -111,7 +111,7 @@ # %% # iris - Classic flower measurements dataset iris = data('iris') -(ggplot(iris, aes(x='sepal_length', y='sepal_width', color='Species')) +(ggplot(iris, aes(x='sepal_length', y='sepal_width', color='species')) + geom_point(size=8) + labs(title='Iris Dataset')) diff --git a/examples/maps.ipynb b/examples/maps.ipynb index a6e3577..dd0eb17 100644 --- a/examples/maps.ipynb +++ b/examples/maps.ipynb @@ -438,7 +438,7 @@ ], "metadata": { "kernelspec": { - "display_name": "env (3.12.10)", + "display_name": "venv (3.12.10)", "language": "python", "name": "python3" }, diff --git a/examples/plot.html b/examples/plot.html index 12fa72c..0326fc2 100644 --- a/examples/plot.html +++ b/examples/plot.html @@ -1,3888 +1,7 @@ -
-
+
+
\ No newline at end of file diff --git a/examples/plot_extended.html b/examples/plot_extended.html index ca82eaf..13d7c9a 100644 --- a/examples/plot_extended.html +++ b/examples/plot_extended.html @@ -1,3888 +1,7 @@ -
-
+
+
\ No newline at end of file diff --git a/examples/prices.ipynb b/examples/prices.ipynb index 021908f..651cb12 100644 --- a/examples/prices.ipynb +++ b/examples/prices.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "48c91425", "metadata": {}, "outputs": [], @@ -13,44 +13,17 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "a99e6497", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['commodity_prices',\n", - " 'diamonds',\n", - " 'economics',\n", - " 'economics_long',\n", - " 'faithfuld',\n", - " 'iris',\n", - " 'luv_colours',\n", - " 'midwest',\n", - " 'mpg',\n", - " 'msleep',\n", - " 'mtcars',\n", - " 'presidential',\n", - " 'seals',\n", - " 'txhousing',\n", - " 'us_flights',\n", - " 'us_flights_edges',\n", - " 'us_flights_nodes']" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "data()" ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "id": "bc7a936a", "metadata": {}, "outputs": [], @@ -60,468 +33,17 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "id": "abba81f9", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.microsoft.datawrangler.viewer.v0+json": { - "columns": [ - { - "name": "index", - "rawType": "int64", - "type": "integer" - }, - { - "name": "date", - "rawType": "object", - "type": "string" - }, - { - "name": "series", - "rawType": "object", - "type": "string" - }, - { - "name": "value", - "rawType": "float64", - "type": "float" - } - ], - "ref": "35bbd0a6-9cbf-4bdc-ad3c-fc4e4e73a2f4", - "rows": [ - [ - "0", - "2007-01-02", - "CL01", - "61.05" - ], - [ - "1", - "2007-01-02", - "CL02", - "62.38" - ], - [ - "2", - "2007-01-02", - "CL03", - "63.26" - ], - [ - "3", - "2007-01-02", - "CL04", - "63.95" - ], - [ - "4", - "2007-01-02", - "CL05", - "64.54" - ], - [ - "5", - "2007-01-02", - "CL06", - "65.04" - ], - [ - "6", - "2007-01-02", - "CL07", - "65.49" - ], - [ - "7", - "2007-01-02", - "CL08", - "65.89" - ], - [ - "8", - "2007-01-02", - "CL09", - "66.23" - ], - [ - "9", - "2007-01-02", - "CL10", - "66.53" - ], - [ - "10", - "2007-01-02", - "CL11", - "66.79" - ], - [ - "11", - "2007-01-02", - "CL12", - "67.01" - ], - [ - "12", - "2007-01-02", - "CL13", - "67.18" - ], - [ - "13", - "2007-01-02", - "CL14", - "67.31" - ], - [ - "14", - "2007-01-02", - "CL15", - "67.43" - ], - [ - "15", - "2007-01-02", - "CL16", - "67.51" - ], - [ - "16", - "2007-01-02", - "CL17", - "67.57" - ], - [ - "17", - "2007-01-02", - "CL18", - "67.6" - ], - [ - "18", - "2007-01-02", - "CL19", - "67.61" - ], - [ - "19", - "2007-01-02", - "CL20", - "67.59" - ], - [ - "20", - "2007-01-02", - "CL21", - "67.56" - ], - [ - "21", - "2007-01-02", - "CL22", - "67.53" - ], - [ - "22", - "2007-01-02", - "CL23", - "67.5" - ], - [ - "23", - "2007-01-02", - "CL24", - "67.48" - ], - [ - "24", - "2007-01-02", - "CL25", - "67.44" - ], - [ - "25", - "2007-01-02", - "CL26", - "67.39" - ], - [ - "26", - "2007-01-02", - "CL27", - "67.34" - ], - [ - "27", - "2007-01-02", - "CL28", - "67.29" - ], - [ - "28", - "2007-01-02", - "CL29", - "67.24" - ], - [ - "29", - "2007-01-02", - "CL30", - "67.19" - ], - [ - "30", - "2007-01-02", - "CL31", - "67.14" - ], - [ - "31", - "2007-01-02", - "CL32", - "67.08" - ], - [ - "32", - "2007-01-02", - "CL33", - "67.02" - ], - [ - "33", - "2007-01-02", - "CL34", - "66.96" - ], - [ - "34", - "2007-01-02", - "CL35", - "66.9" - ], - [ - "35", - "2007-01-02", - "CL36", - "66.84" - ], - [ - "36", - "2007-01-02", - "NG01", - "8.888" - ], - [ - "37", - "2007-01-02", - "NG02", - "8.903" - ], - [ - "38", - "2007-01-02", - "NG03", - "8.698" - ], - [ - "39", - "2007-01-02", - "NG04", - "7.558" - ], - [ - "40", - "2007-01-02", - "NG05", - "7.453" - ], - [ - "41", - "2007-01-02", - "NG06", - "7.508" - ], - [ - "42", - "2007-01-02", - "NG07", - "7.593" - ], - [ - "43", - "2007-01-02", - "NG08", - "7.653" - ], - [ - "44", - "2007-01-02", - "NG09", - "7.718" - ], - [ - "45", - "2007-01-02", - "NG10", - "7.803" - ], - [ - "46", - "2007-01-02", - "NG11", - "8.238" - ], - [ - "47", - "2007-01-02", - "NG12", - "8.673" - ], - [ - "48", - "2007-01-02", - "NG13", - "8.888" - ], - [ - "49", - "2007-01-02", - "NG14", - "8.893" - ] - ], - "shape": { - "columns": 3, - "rows": 692629 - } - }, - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
dateseriesvalue
02007-01-02CL0161.05
12007-01-02CL0262.38
22007-01-02CL0363.26
32007-01-02CL0463.95
42007-01-02CL0564.54
............
6926242023-10-19MJP02110.00
6926252023-10-19MJP03110.00
6926262023-10-19MJP04NaN
6926272023-10-19MJP05NaN
6926282023-10-19MJP06NaN
\n", - "

692629 rows × 3 columns

\n", - "
" - ], - "text/plain": [ - " date series value\n", - "0 2007-01-02 CL01 61.05\n", - "1 2007-01-02 CL02 62.38\n", - "2 2007-01-02 CL03 63.26\n", - "3 2007-01-02 CL04 63.95\n", - "4 2007-01-02 CL05 64.54\n", - "... ... ... ...\n", - "692624 2023-10-19 MJP02 110.00\n", - "692625 2023-10-19 MJP03 110.00\n", - "692626 2023-10-19 MJP04 NaN\n", - "692627 2023-10-19 MJP05 NaN\n", - "692628 2023-10-19 MJP06 NaN\n", - "\n", - "[692629 rows x 3 columns]" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "id": "12ab4e21", "metadata": {}, "outputs": [], @@ -531,864 +53,22 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "id": "25c14b98", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.microsoft.datawrangler.viewer.v0+json": { - "columns": [ - { - "name": "index", - "rawType": "int64", - "type": "integer" - }, - { - "name": "date", - "rawType": "datetime64[ns]", - "type": "datetime" - }, - { - "name": "series", - "rawType": "object", - "type": "string" - }, - { - "name": "value", - "rawType": "float64", - "type": "float" - } - ], - "ref": "60e1d2ce-8911-49eb-b650-7e618bd5c66e", - "rows": [ - [ - "0", - "2007-01-02 00:00:00", - "CL01", - "61.05" - ], - [ - "1", - "2007-01-02 00:00:00", - "CL02", - "62.38" - ], - [ - "2", - "2007-01-02 00:00:00", - "CL03", - "63.26" - ], - [ - "3", - "2007-01-02 00:00:00", - "CL04", - "63.95" - ], - [ - "4", - "2007-01-02 00:00:00", - "CL05", - "64.54" - ], - [ - "5", - "2007-01-02 00:00:00", - "CL06", - "65.04" - ], - [ - "6", - "2007-01-02 00:00:00", - "CL07", - "65.49" - ], - [ - "7", - "2007-01-02 00:00:00", - "CL08", - "65.89" - ], - [ - "8", - "2007-01-02 00:00:00", - "CL09", - "66.23" - ], - [ - "9", - "2007-01-02 00:00:00", - "CL10", - "66.53" - ], - [ - "10", - "2007-01-02 00:00:00", - "CL11", - "66.79" - ], - [ - "11", - "2007-01-02 00:00:00", - "CL12", - "67.01" - ], - [ - "12", - "2007-01-02 00:00:00", - "CL13", - "67.18" - ], - [ - "13", - "2007-01-02 00:00:00", - "CL14", - "67.31" - ], - [ - "14", - "2007-01-02 00:00:00", - "CL15", - "67.43" - ], - [ - "15", - "2007-01-02 00:00:00", - "CL16", - "67.51" - ], - [ - "16", - "2007-01-02 00:00:00", - "CL17", - "67.57" - ], - [ - "17", - "2007-01-02 00:00:00", - "CL18", - "67.6" - ], - [ - "18", - "2007-01-02 00:00:00", - "CL19", - "67.61" - ], - [ - "19", - "2007-01-02 00:00:00", - "CL20", - "67.59" - ], - [ - "20", - "2007-01-02 00:00:00", - "CL21", - "67.56" - ], - [ - "21", - "2007-01-02 00:00:00", - "CL22", - "67.53" - ], - [ - "22", - "2007-01-02 00:00:00", - "CL23", - "67.5" - ], - [ - "23", - "2007-01-02 00:00:00", - "CL24", - "67.48" - ], - [ - "24", - "2007-01-02 00:00:00", - "CL25", - "67.44" - ], - [ - "25", - "2007-01-02 00:00:00", - "CL26", - "67.39" - ], - [ - "26", - "2007-01-02 00:00:00", - "CL27", - "67.34" - ], - [ - "27", - "2007-01-02 00:00:00", - "CL28", - "67.29" - ], - [ - "28", - "2007-01-02 00:00:00", - "CL29", - "67.24" - ], - [ - "29", - "2007-01-02 00:00:00", - "CL30", - "67.19" - ], - [ - "30", - "2007-01-02 00:00:00", - "CL31", - "67.14" - ], - [ - "31", - "2007-01-02 00:00:00", - "CL32", - "67.08" - ], - [ - "32", - "2007-01-02 00:00:00", - "CL33", - "67.02" - ], - [ - "33", - "2007-01-02 00:00:00", - "CL34", - "66.96" - ], - [ - "34", - "2007-01-02 00:00:00", - "CL35", - "66.9" - ], - [ - "35", - "2007-01-02 00:00:00", - "CL36", - "66.84" - ], - [ - "36", - "2007-01-02 00:00:00", - "NG01", - "8.888" - ], - [ - "37", - "2007-01-02 00:00:00", - "NG02", - "8.903" - ], - [ - "38", - "2007-01-02 00:00:00", - "NG03", - "8.698" - ], - [ - "39", - "2007-01-02 00:00:00", - "NG04", - "7.558" - ], - [ - "40", - "2007-01-02 00:00:00", - "NG05", - "7.453" - ], - [ - "41", - "2007-01-02 00:00:00", - "NG06", - "7.508" - ], - [ - "42", - "2007-01-02 00:00:00", - "NG07", - "7.593" - ], - [ - "43", - "2007-01-02 00:00:00", - "NG08", - "7.653" - ], - [ - "44", - "2007-01-02 00:00:00", - "NG09", - "7.718" - ], - [ - "45", - "2007-01-02 00:00:00", - "NG10", - "7.803" - ], - [ - "46", - "2007-01-02 00:00:00", - "NG11", - "8.238" - ], - [ - "47", - "2007-01-02 00:00:00", - "NG12", - "8.673" - ], - [ - "48", - "2007-01-02 00:00:00", - "NG13", - "8.888" - ], - [ - "49", - "2007-01-02 00:00:00", - "NG14", - "8.893" - ] - ], - "shape": { - "columns": 3, - "rows": 692629 - } - }, - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
dateseriesvalue
02007-01-02CL0161.05
12007-01-02CL0262.38
22007-01-02CL0363.26
32007-01-02CL0463.95
42007-01-02CL0564.54
............
6926242023-10-19MJP02110.00
6926252023-10-19MJP03110.00
6926262023-10-19MJP04NaN
6926272023-10-19MJP05NaN
6926282023-10-19MJP06NaN
\n", - "

692629 rows × 3 columns

\n", - "
" - ], - "text/plain": [ - " date series value\n", - "0 2007-01-02 CL01 61.05\n", - "1 2007-01-02 CL02 62.38\n", - "2 2007-01-02 CL03 63.26\n", - "3 2007-01-02 CL04 63.95\n", - "4 2007-01-02 CL05 64.54\n", - "... ... ... ...\n", - "692624 2023-10-19 MJP02 110.00\n", - "692625 2023-10-19 MJP03 110.00\n", - "692626 2023-10-19 MJP04 NaN\n", - "692627 2023-10-19 MJP05 NaN\n", - "692628 2023-10-19 MJP06 NaN\n", - "\n", - "[692629 rows x 3 columns]" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "id": "8fcfa611", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.microsoft.datawrangler.viewer.v0+json": { - "columns": [ - { - "name": "date", - "rawType": "datetime64[ns]", - "type": "datetime" - }, - { - "name": "series", - "rawType": "object", - "type": "string" - }, - { - "name": "value", - "rawType": "float64", - "type": "float" - } - ], - "ref": "2fdd3185-2c7e-42e9-b058-572191657e54", - "rows": [ - [ - "2007-01-02 00:00:00", - "CL01", - "61.05" - ], - [ - "2007-01-03 00:00:00", - "CL01", - "58.32" - ], - [ - "2007-01-04 00:00:00", - "CL01", - "55.59" - ], - [ - "2007-01-05 00:00:00", - "CL01", - "56.31" - ], - [ - "2007-01-08 00:00:00", - "CL01", - "56.09" - ], - [ - "2007-01-09 00:00:00", - "CL01", - "55.64" - ], - [ - "2007-01-10 00:00:00", - "CL01", - "54.02" - ], - [ - "2007-01-11 00:00:00", - "CL01", - "51.88" - ], - [ - "2007-01-12 00:00:00", - "CL01", - "52.99" - ], - [ - "2007-01-16 00:00:00", - "CL01", - "51.21" - ], - [ - "2007-01-17 00:00:00", - "CL01", - "52.24" - ], - [ - "2007-01-18 00:00:00", - "CL01", - "50.48" - ], - [ - "2007-01-19 00:00:00", - "CL01", - "51.99" - ], - [ - "2007-01-22 00:00:00", - "CL01", - "51.13" - ], - [ - "2007-01-23 00:00:00", - "CL01", - "55.04" - ], - [ - "2007-01-24 00:00:00", - "CL01", - "55.37" - ], - [ - "2007-01-25 00:00:00", - "CL01", - "54.23" - ], - [ - "2007-01-26 00:00:00", - "CL01", - "55.42" - ], - [ - "2007-01-29 00:00:00", - "CL01", - "54.01" - ], - [ - "2007-01-30 00:00:00", - "CL01", - "56.97" - ], - [ - "2007-01-31 00:00:00", - "CL01", - "58.14" - ], - [ - "2007-02-01 00:00:00", - "CL01", - "57.3" - ], - [ - "2007-02-02 00:00:00", - "CL01", - "59.02" - ], - [ - "2007-02-05 00:00:00", - "CL01", - "58.74" - ], - [ - "2007-02-06 00:00:00", - "CL01", - "58.88" - ], - [ - "2007-02-07 00:00:00", - "CL01", - "57.71" - ], - [ - "2007-02-08 00:00:00", - "CL01", - "59.71" - ], - [ - "2007-02-09 00:00:00", - "CL01", - "59.89" - ], - [ - "2007-02-12 00:00:00", - "CL01", - "57.81" - ], - [ - "2007-02-13 00:00:00", - "CL01", - "59.06" - ], - [ - "2007-02-14 00:00:00", - "CL01", - "58.0" - ], - [ - "2007-02-15 00:00:00", - "CL01", - "57.99" - ], - [ - "2007-02-16 00:00:00", - "CL01", - "59.39" - ], - [ - "2007-02-20 00:00:00", - "CL01", - "58.07" - ], - [ - "2007-02-21 00:00:00", - "CL01", - "60.07" - ], - [ - "2007-02-22 00:00:00", - "CL01", - "60.95" - ], - [ - "2007-02-23 00:00:00", - "CL01", - "61.14" - ], - [ - "2007-02-26 00:00:00", - "CL01", - "61.39" - ], - [ - "2007-02-27 00:00:00", - "CL01", - "61.46" - ], - [ - "2007-02-28 00:00:00", - "CL01", - "61.79" - ], - [ - "2007-03-01 00:00:00", - "CL01", - "62.0" - ], - [ - "2007-03-02 00:00:00", - "CL01", - "61.64" - ], - [ - "2007-03-05 00:00:00", - "CL01", - "60.07" - ], - [ - "2007-03-06 00:00:00", - "CL01", - "60.69" - ], - [ - "2007-03-07 00:00:00", - "CL01", - "61.82" - ], - [ - "2007-03-08 00:00:00", - "CL01", - "61.64" - ], - [ - "2007-03-09 00:00:00", - "CL01", - "60.05" - ], - [ - "2007-03-12 00:00:00", - "CL01", - "58.91" - ], - [ - "2007-03-13 00:00:00", - "CL01", - "57.93" - ], - [ - "2007-03-14 00:00:00", - "CL01", - "58.16" - ] - ], - "shape": { - "columns": 2, - "rows": 4233 - } - }, - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
seriesvalue
date
2007-01-02CL0161.05
2007-01-03CL0158.32
2007-01-04CL0155.59
2007-01-05CL0156.31
2007-01-08CL0156.09
.........
2023-10-13CL0187.69
2023-10-16CL0186.66
2023-10-17CL0186.66
2023-10-18CL0188.32
2023-10-19CL0189.37
\n", - "

4233 rows × 2 columns

\n", - "
" - ], - "text/plain": [ - " series value\n", - "date \n", - "2007-01-02 CL01 61.05\n", - "2007-01-03 CL01 58.32\n", - "2007-01-04 CL01 55.59\n", - "2007-01-05 CL01 56.31\n", - "2007-01-08 CL01 56.09\n", - "... ... ...\n", - "2023-10-13 CL01 87.69\n", - "2023-10-16 CL01 86.66\n", - "2023-10-17 CL01 86.66\n", - "2023-10-18 CL01 88.32\n", - "2023-10-19 CL01 89.37\n", - "\n", - "[4233 rows x 2 columns]" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "df = df.set_index('date')\n", - "df[df.series == 'CL01']" + "df = df[df.series == 'CL01'].set_index('date').value" ] }, { @@ -1422,7 +102,7 @@ ], "metadata": { "kernelspec": { - "display_name": "env (3.12.10)", + "display_name": "venv (3.12.10)", "language": "python", "name": "python3" }, diff --git a/examples/session_changes.ipynb b/examples/session_changes.ipynb new file mode 100644 index 0000000..a76575b --- /dev/null +++ b/examples/session_changes.ipynb @@ -0,0 +1,884 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Session Changes - Visual Verification\n", + "\n", + "This notebook demonstrates all changes made in the current development session.\n", + "\n", + "## Contents\n", + "1. **New Geoms**\n", + " - `geom_rect` - Rectangles for highlighting regions\n", + " - `geom_label` - Text labels with background boxes\n", + "\n", + "2. **New Scales**\n", + " - `scale_x_reverse` / `scale_y_reverse` - Reversed axes\n", + "\n", + "3. **New Coordinates**\n", + " - `coord_fixed` - Fixed aspect ratio\n", + "\n", + "4. **New Parameters**\n", + " - `stroke` for `geom_point` (marker border width)\n", + " - `arrow` and `arrow_size` for `geom_segment`\n", + " - `width` for `geom_errorbar` (cap width)\n", + " - `linewidth` alias for `size` (ggplot2 3.4+ compatibility)\n", + " - `parse` for `geom_text` (LaTeX/MathJax support)\n", + "\n", + "5. **New Position Exports**\n", + " - `position_fill`, `position_nudge`, `position_identity`, `position_dodge2`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "from ggplotly import (\n", + " ggplot, aes, labs,\n", + " # New geoms\n", + " geom_rect, geom_label,\n", + " # New scales\n", + " scale_x_reverse, scale_y_reverse,\n", + " # New coords\n", + " coord_fixed,\n", + " # Existing geoms with new parameters\n", + " geom_point, geom_segment, geom_errorbar, geom_line, geom_text,\n", + " geom_bar, geom_boxplot, geom_path,\n", + " # Positions\n", + " position_fill, position_nudge, position_identity, position_dodge2,\n", + " # Other\n", + " theme_minimal, facet_wrap\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 1. geom_rect - Rectangles\n", + "\n", + "Draw rectangles defined by corner coordinates (xmin, xmax, ymin, ymax)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Basic Rectangle" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rect_df = pd.DataFrame({\n", + " 'xmin': [1, 4],\n", + " 'xmax': [3, 6],\n", + " 'ymin': [1, 3],\n", + " 'ymax': [4, 6]\n", + "})\n", + "\n", + "(\n", + " ggplot(rect_df, aes(xmin='xmin', xmax='xmax', ymin='ymin', ymax='ymax'))\n", + " + geom_rect()\n", + " + labs(title='Basic geom_rect')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Rectangle with Custom Styling" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " ggplot(rect_df, aes(xmin='xmin', xmax='xmax', ymin='ymin', ymax='ymax'))\n", + " + geom_rect(fill='lightblue', color='navy', size=2, alpha=0.7)\n", + " + labs(title='geom_rect with fill, border color, and alpha')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Rectangle with Fill Mapped to Category" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rect_cat_df = pd.DataFrame({\n", + " 'xmin': [1, 4, 7],\n", + " 'xmax': [3, 6, 9],\n", + " 'ymin': [1, 2, 1],\n", + " 'ymax': [4, 5, 3],\n", + " 'category': ['A', 'B', 'C']\n", + "})\n", + "\n", + "(\n", + " ggplot(rect_cat_df, aes(xmin='xmin', xmax='xmax', ymin='ymin', ymax='ymax', fill='category'))\n", + " + geom_rect(alpha=0.6)\n", + " + labs(title='geom_rect with fill aesthetic')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Rectangle as Highlight Overlay" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Scatter data\n", + "np.random.seed(42)\n", + "scatter_df = pd.DataFrame({\n", + " 'x': np.random.uniform(0, 10, 50),\n", + " 'y': np.random.uniform(0, 10, 50)\n", + "})\n", + "\n", + "# Highlight region\n", + "highlight_df = pd.DataFrame({\n", + " 'xmin': [3], 'xmax': [7],\n", + " 'ymin': [4], 'ymax': [8]\n", + "})\n", + "\n", + "(\n", + " ggplot(scatter_df, aes(x='x', y='y'))\n", + " + geom_rect(\n", + " data=highlight_df,\n", + " mapping=aes(xmin='xmin', xmax='xmax', ymin='ymin', ymax='ymax'),\n", + " fill='yellow', alpha=0.3\n", + " )\n", + " + geom_point(color='steelblue', size=8)\n", + " + labs(title='geom_rect as highlight overlay on scatter plot')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 2. geom_label - Text Labels with Background\n", + "\n", + "Like `geom_text` but with a visible background box for better readability." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Basic Labels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "label_df = pd.DataFrame({\n", + " 'x': [1, 2, 3, 4],\n", + " 'y': [2, 4, 3, 5],\n", + " 'name': ['Alpha', 'Beta', 'Gamma', 'Delta']\n", + "})\n", + "\n", + "(\n", + " ggplot(label_df, aes(x='x', y='y', label='name'))\n", + " + geom_label()\n", + " + labs(title='Basic geom_label')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Styled Labels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " ggplot(label_df, aes(x='x', y='y', label='name'))\n", + " + geom_label(fill='lightgreen', color='darkgreen', size=14, alpha=0.9)\n", + " + labs(title='geom_label with custom fill, color, and size')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Labels with Points and Nudge" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " ggplot(label_df, aes(x='x', y='y'))\n", + " + geom_point(size=12, color='coral')\n", + " + geom_label(aes(label='name'), nudge_y=0.4, fill='white', size=10)\n", + " + labs(title='geom_label with nudge_y to offset from points')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Labels with Fill Mapped to Category" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "label_cat_df = pd.DataFrame({\n", + " 'x': [1, 2, 3, 4],\n", + " 'y': [2, 4, 3, 5],\n", + " 'name': ['A', 'B', 'C', 'D'],\n", + " 'group': ['Group 1', 'Group 1', 'Group 2', 'Group 2']\n", + "})\n", + "\n", + "(\n", + " ggplot(label_cat_df, aes(x='x', y='y', label='name', fill='group'))\n", + " + geom_label(size=14)\n", + " + labs(title='geom_label with fill aesthetic mapped to category')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### geom_label vs geom_text Comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "compare_df = pd.DataFrame({\n", + " 'x': [1, 2],\n", + " 'y': [1, 1],\n", + " 'label': ['geom_text', 'geom_label'],\n", + " 'type': ['text', 'label']\n", + "})\n", + "\n", + "# Create a busy background\n", + "bg_df = pd.DataFrame({\n", + " 'x': np.random.uniform(0.5, 2.5, 100),\n", + " 'y': np.random.uniform(0.5, 1.5, 100)\n", + "})\n", + "\n", + "(\n", + " ggplot(bg_df, aes(x='x', y='y'))\n", + " + geom_point(alpha=0.3, size=6)\n", + " + geom_text(data=compare_df[compare_df['type']=='text'], \n", + " mapping=aes(x='x', y='y', label='label'), size=14, color='red')\n", + " + geom_label(data=compare_df[compare_df['type']=='label'],\n", + " mapping=aes(x='x', y='y', label='label'), size=14, color='red', fill='white')\n", + " + labs(title='geom_text (left) vs geom_label (right) - label has background')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 2.5 scale_x_reverse / scale_y_reverse - Reversed Axes\n", + "\n", + "Reverse the direction of axes. Useful for depth charts, rankings, or inverted coordinate systems." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### scale_x_reverse - Reversed X-axis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Normal vs Reversed X-axis comparison\n", + "reverse_df = pd.DataFrame({\n", + " 'x': [1, 2, 3, 4, 5],\n", + " 'y': [2, 4, 3, 5, 4]\n", + "})\n", + "\n", + "(\n", + " ggplot(reverse_df, aes(x='x', y='y'))\n", + " + geom_line(color='steelblue', size=2)\n", + " + geom_point(color='steelblue', size=10)\n", + " + scale_x_reverse()\n", + " + labs(title='scale_x_reverse - X-axis runs from 5 to 1')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### scale_y_reverse - Reversed Y-axis\n", + "\n", + "Useful for depth charts, rankings, or inverted visualizations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Depth chart example - useful for ocean/ground depth, rankings, etc.\n", + "depth_df = pd.DataFrame({\n", + " 'depth': [0, 10, 20, 30, 40, 50],\n", + " 'temperature': [25, 22, 18, 15, 12, 10]\n", + "})\n", + "\n", + "(\n", + " ggplot(depth_df, aes(x='temperature', y='depth'))\n", + " + geom_line(color='darkblue', size=2)\n", + " + geom_point(color='darkblue', size=10)\n", + " + scale_y_reverse()\n", + " + labs(title='scale_y_reverse - Ocean Temperature Profile',\n", + " x='Temperature (°C)', y='Depth (m)')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### coord_fixed - Fixed Aspect Ratio\n", + "\n", + "Essential for maps and geometric visualizations where the x/y ratio matters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# coord_fixed ensures 1 unit on x = 1 unit on y\n", + "circle_t = np.linspace(0, 2 * np.pi, 100)\n", + "circle_df = pd.DataFrame({\n", + " 'x': np.cos(circle_t),\n", + " 'y': np.sin(circle_t)\n", + "})\n", + "\n", + "(\n", + " ggplot(circle_df, aes(x='x', y='y'))\n", + " + geom_path(color='coral', size=2)\n", + " + coord_fixed(ratio=1)\n", + " + labs(title='coord_fixed(ratio=1) - Circle appears as circle, not ellipse')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### coord_fixed with Different Ratios" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Different ratio - 2:1 means y is stretched (1 y unit = 2 x units)\n", + "square_df = pd.DataFrame({\n", + " 'x': [0, 1, 1, 0, 0],\n", + " 'y': [0, 0, 1, 1, 0]\n", + "})\n", + "\n", + "(\n", + " ggplot(square_df, aes(x='x', y='y'))\n", + " + geom_path(color='forestgreen', size=2)\n", + " + coord_fixed(ratio=2)\n", + " + labs(title='coord_fixed(ratio=2) - Square becomes tall rectangle (y stretched)')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 3. New Parameters for Existing Geoms" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### geom_point: `stroke` parameter\n", + "\n", + "Controls the border width around markers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stroke_df = pd.DataFrame({\n", + " 'x': [1, 2, 3, 4, 5],\n", + " 'y': [1, 1, 1, 1, 1],\n", + " 'stroke_val': [0, 1, 2, 3, 4]\n", + "})\n", + "\n", + "(\n", + " ggplot(stroke_df, aes(x='x', y='y'))\n", + " + geom_point(data=stroke_df[stroke_df['x']==1], stroke=0, size=20, color='steelblue')\n", + " + geom_point(data=stroke_df[stroke_df['x']==2], stroke=1, size=20, color='steelblue')\n", + " + geom_point(data=stroke_df[stroke_df['x']==3], stroke=2, size=20, color='steelblue')\n", + " + geom_point(data=stroke_df[stroke_df['x']==4], stroke=3, size=20, color='steelblue')\n", + " + geom_point(data=stroke_df[stroke_df['x']==5], stroke=4, size=20, color='steelblue')\n", + " + geom_text(aes(label='stroke_val'), nudge_y=0.15, size=12)\n", + " + labs(title='geom_point stroke parameter (0, 1, 2, 3, 4)', y='')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### geom_segment: `arrow` parameter\n", + "\n", + "Adds arrowheads to line segments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "arrow_df = pd.DataFrame({\n", + " 'x': [1, 1, 1],\n", + " 'y': [1, 2, 3],\n", + " 'xend': [3, 3, 3],\n", + " 'yend': [1.5, 2.5, 3.5]\n", + "})\n", + "\n", + "(\n", + " ggplot(arrow_df, aes(x='x', y='y', xend='xend', yend='yend'))\n", + " + geom_segment(arrow=True, arrow_size=15, color='darkblue', size=2)\n", + " + labs(title='geom_segment with arrow=True')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Arrow Size Comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "arrow_size_df = pd.DataFrame({\n", + " 'x': [1, 1, 1],\n", + " 'y': [1, 2, 3],\n", + " 'xend': [3, 3, 3],\n", + " 'yend': [1, 2, 3],\n", + " 'size_label': ['arrow_size=10', 'arrow_size=20', 'arrow_size=30']\n", + "})\n", + "\n", + "(\n", + " ggplot()\n", + " + geom_segment(data=arrow_size_df[arrow_size_df['y']==1],\n", + " mapping=aes(x='x', y='y', xend='xend', yend='yend'),\n", + " arrow=True, arrow_size=10, color='coral')\n", + " + geom_segment(data=arrow_size_df[arrow_size_df['y']==2],\n", + " mapping=aes(x='x', y='y', xend='xend', yend='yend'),\n", + " arrow=True, arrow_size=20, color='coral')\n", + " + geom_segment(data=arrow_size_df[arrow_size_df['y']==3],\n", + " mapping=aes(x='x', y='y', xend='xend', yend='yend'),\n", + " arrow=True, arrow_size=30, color='coral')\n", + " + geom_label(data=arrow_size_df, mapping=aes(x='xend', y='yend', label='size_label'),\n", + " nudge_x=0.5, hjust=0, size=10)\n", + " + labs(title='geom_segment arrow_size comparison')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### geom_errorbar: `width` parameter\n", + "\n", + "Controls the width of error bar caps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "error_df = pd.DataFrame({\n", + " 'x': [1, 2, 3],\n", + " 'y': [5, 7, 6],\n", + " 'ymin': [4, 5.5, 4.5],\n", + " 'ymax': [6, 8.5, 7.5],\n", + " 'width_label': ['width=2', 'width=8', 'width=15']\n", + "})\n", + "\n", + "(\n", + " ggplot()\n", + " + geom_errorbar(data=error_df[error_df['x']==1],\n", + " mapping=aes(x='x', y='y', ymin='ymin', ymax='ymax'),\n", + " width=2, color='purple')\n", + " + geom_errorbar(data=error_df[error_df['x']==2],\n", + " mapping=aes(x='x', y='y', ymin='ymin', ymax='ymax'),\n", + " width=8, color='purple')\n", + " + geom_errorbar(data=error_df[error_df['x']==3],\n", + " mapping=aes(x='x', y='y', ymin='ymin', ymax='ymax'),\n", + " width=15, color='purple')\n", + " + geom_point(data=error_df, mapping=aes(x='x', y='y'), size=10, color='purple')\n", + " + geom_text(data=error_df, mapping=aes(x='x', y='ymax', label='width_label'),\n", + " nudge_y=0.5, size=10)\n", + " + labs(title='geom_errorbar width parameter comparison')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### geom_line: `linewidth` alias\n", + "\n", + "The `linewidth` parameter is now an alias for `size` (ggplot2 3.4+ compatibility)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "line_df = pd.DataFrame({\n", + " 'x': [1, 2, 3, 4, 5],\n", + " 'y1': [1, 2, 1.5, 3, 2.5],\n", + " 'y2': [2, 3, 2.5, 4, 3.5],\n", + " 'y3': [3, 4, 3.5, 5, 4.5]\n", + "})\n", + "\n", + "(\n", + " ggplot(line_df, aes(x='x'))\n", + " + geom_line(aes(y='y1'), linewidth=1, color='blue') # Using new linewidth\n", + " + geom_line(aes(y='y2'), linewidth=3, color='green') # Using new linewidth\n", + " + geom_line(aes(y='y3'), size=5, color='red') # Using old size (still works)\n", + " + labs(title='linewidth=1 (blue), linewidth=3 (green), size=5 (red)')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### geom_text: `parse` parameter\n", + "\n", + "Enables LaTeX/MathJax rendering for mathematical expressions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "math_df = pd.DataFrame({\n", + " 'x': [1, 2, 3],\n", + " 'y': [1, 2, 3],\n", + " 'formula': ['\\\\alpha', '\\\\beta^2', '\\\\gamma + \\\\delta']\n", + "})\n", + "\n", + "(\n", + " ggplot(math_df, aes(x='x', y='y', label='formula'))\n", + " + geom_point(size=12, color='gray')\n", + " + geom_text(parse=True, size=16, nudge_y=0.2)\n", + " + labs(title='geom_text with parse=True (LaTeX rendering)')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 4. New Position Exports" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### position_fill - Stacked bars normalized to 100%" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pos_df = pd.DataFrame({\n", + " 'category': ['A', 'A', 'B', 'B', 'C', 'C'],\n", + " 'group': ['X', 'Y', 'X', 'Y', 'X', 'Y'],\n", + " 'value': [10, 20, 15, 25, 8, 12]\n", + "})\n", + "\n", + "(\n", + " ggplot(pos_df, aes(x='category', y='value', fill='group'))\n", + " + geom_bar(stat='identity', position=position_fill())\n", + " + labs(title='position_fill - stacked bars normalized to 100%')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### position_nudge - Offset labels from points" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nudge_df = pd.DataFrame({\n", + " 'x': [1, 2, 3],\n", + " 'y': [1, 2, 3],\n", + " 'label': ['Point A', 'Point B', 'Point C']\n", + "})\n", + "\n", + "(\n", + " ggplot(nudge_df, aes(x='x', y='y'))\n", + " + geom_point(size=12, color='tomato')\n", + " + geom_text(aes(label='label'), position=position_nudge(x=0.2, y=0.2))\n", + " + labs(title='position_nudge - offset text from points')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### position_dodge2 - Dodge without grouping variable" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dodge_df = pd.DataFrame({\n", + " 'category': ['A', 'A', 'B', 'B'],\n", + " 'type': ['X', 'Y', 'X', 'Y'],\n", + " 'value': [10, 15, 12, 18]\n", + "})\n", + "\n", + "(\n", + " ggplot(dodge_df, aes(x='category', y='value', fill='type'))\n", + " + geom_bar(stat='identity', position=position_dodge2())\n", + " + labs(title='position_dodge2 - side-by-side bars')\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 5. Combined Example\n", + "\n", + "A comprehensive plot using multiple new features together." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create sample data\n", + "np.random.seed(123)\n", + "combined_df = pd.DataFrame({\n", + " 'x': range(1, 11),\n", + " 'y': np.cumsum(np.random.randn(10)) + 10,\n", + "})\n", + "combined_df['label'] = combined_df['y'].round(1).astype(str)\n", + "\n", + "# Highlight region\n", + "highlight = pd.DataFrame({\n", + " 'xmin': [3], 'xmax': [7],\n", + " 'ymin': [combined_df['y'].min() - 1],\n", + " 'ymax': [combined_df['y'].max() + 1]\n", + "})\n", + "\n", + "# Arrow annotation\n", + "arrow_annot = pd.DataFrame({\n", + " 'x': [8], 'y': [combined_df['y'].iloc[7] + 2],\n", + " 'xend': [8], 'yend': [combined_df['y'].iloc[7] + 0.3]\n", + "})\n", + "\n", + "(\n", + " ggplot(combined_df, aes(x='x', y='y'))\n", + " # Highlight region using geom_rect\n", + " + geom_rect(\n", + " data=highlight,\n", + " mapping=aes(xmin='xmin', xmax='xmax', ymin='ymin', ymax='ymax'),\n", + " fill='lightyellow', alpha=0.5\n", + " )\n", + " # Line with linewidth\n", + " + geom_line(linewidth=2, color='steelblue')\n", + " # Points with stroke\n", + " + geom_point(size=12, color='steelblue', stroke=2)\n", + " # Arrow annotation using geom_segment with arrow\n", + " + geom_segment(\n", + " data=arrow_annot,\n", + " mapping=aes(x='x', y='y', xend='xend', yend='yend'),\n", + " arrow=True, arrow_size=12, color='red'\n", + " )\n", + " # Label for arrow\n", + " + geom_label(\n", + " data=pd.DataFrame({'x': [8], 'y': [combined_df['y'].iloc[7] + 2.5], 'label': ['Peak!']}),\n", + " mapping=aes(x='x', y='y', label='label'),\n", + " fill='white', color='red', size=12\n", + " )\n", + " + labs(\n", + " title='Combined Example: rect, label, stroke, arrow, linewidth',\n", + " x='Time', y='Value'\n", + " )\n", + " + theme_minimal()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Summary of Changes\n", + "\n", + "| Feature | Type | Description |\n", + "|---------|------|-------------|\n", + "| `geom_rect` | New Geom | Rectangles defined by xmin/xmax/ymin/ymax |\n", + "| `geom_label` | New Geom | Text labels with background boxes |\n", + "| `scale_x_reverse` | New Scale | Reversed x-axis direction |\n", + "| `scale_y_reverse` | New Scale | Reversed y-axis direction |\n", + "| `coord_fixed` | New Coord | Fixed aspect ratio (e.g., for maps) |\n", + "| `stroke` | New Param | Border width for `geom_point` markers |\n", + "| `arrow`, `arrow_size` | New Params | Arrowheads for `geom_segment` |\n", + "| `width` | New Param | Cap width for `geom_errorbar` |\n", + "| `linewidth` | New Alias | Alias for `size` in line geoms (ggplot2 3.4+) |\n", + "| `parse` | New Param | LaTeX rendering for `geom_text` |\n", + "| `position_fill` | Export | Stacked bars normalized to 100% |\n", + "| `position_nudge` | Export | Offset elements by fixed amount |\n", + "| `position_identity` | Export | No position adjustment |\n", + "| `position_dodge2` | Export | Dodge without explicit grouping |" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv (3.12.10)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/view_all.ipynb b/examples/view_all.ipynb index 1a9c509..c4ddb12 100644 --- a/examples/view_all.ipynb +++ b/examples/view_all.ipynb @@ -1052,35 +1052,11 @@ "\n", "p.draw()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f1e0cdff", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "134231c8", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "47b3e838", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "env (3.12.10)", + "display_name": "venv (3.12.10)", "language": "python", "name": "python3" }, diff --git a/ggplotly/__init__.py b/ggplotly/__init__.py index dc007c2..cb444c1 100644 --- a/ggplotly/__init__.py +++ b/ggplotly/__init__.py @@ -1,7 +1,7 @@ # __init__.py from .aes import aes, after_stat -from .coords import Coord, coord_cartesian, coord_flip, coord_polar, coord_sf +from .coords import Coord, coord_cartesian, coord_fixed, coord_flip, coord_polar, coord_sf from .datasets import data from .facets import Facet, facet_grid, facet_wrap, label_both, label_value from .geoms import ( @@ -21,6 +21,7 @@ geom_histogram, geom_hline, geom_jitter, + geom_label, geom_line, geom_lines, geom_map, @@ -33,6 +34,7 @@ geom_qq, geom_qq_line, geom_range, + geom_rect, geom_ribbon, geom_rug, geom_sankey, @@ -56,7 +58,15 @@ from .layer import Layer, layer from .limits import lims, xlim, ylim from .map_data import map_data -from .positions import position_dodge, position_jitter, position_stack +from .positions import ( + position_dodge, + position_dodge2, + position_fill, + position_identity, + position_jitter, + position_nudge, + position_stack, +) from .scales import ( scale_color_brewer, scale_color_gradient, @@ -73,8 +83,10 @@ scale_x_log10, scale_x_rangeselector, scale_x_rangeslider, + scale_x_reverse, scale_y_continuous, scale_y_log10, + scale_y_reverse, ) from .stats import ( stat_bin, @@ -134,9 +146,10 @@ "label_value", "label_both", "Coord", + "coord_cartesian", + "coord_fixed", "coord_flip", "coord_polar", - "coord_cartesian", "coord_sf", "xlim", "ylim", @@ -147,7 +160,11 @@ "guide_legend", "guide_colorbar", "position_dodge", + "position_dodge2", + "position_fill", + "position_identity", "position_jitter", + "position_nudge", "position_stack", "geom_point", "geom_line", @@ -166,6 +183,8 @@ "geom_ribbon", "geom_tile", "geom_text", + "geom_label", + "geom_rect", "geom_errorbar", "geom_segment", "geom_step", @@ -205,6 +224,8 @@ "scale_size", "scale_x_log10", "scale_y_log10", + "scale_x_reverse", + "scale_y_reverse", "scale_x_date", "scale_x_datetime", "scale_x_rangeslider", diff --git a/ggplotly/aesthetic_mapper.py b/ggplotly/aesthetic_mapper.py index 03fdb25..9f6624b 100644 --- a/ggplotly/aesthetic_mapper.py +++ b/ggplotly/aesthetic_mapper.py @@ -185,6 +185,9 @@ def resolve_aesthetic(self, aesthetic: str) -> tuple[Any, pd.Series | None, dict Parameters: aesthetic: Name of the aesthetic to resolve (e.g., 'color', 'fill', 'size') + + Raises: + ColumnNotFoundError: If value looks like a column name but doesn't exist """ # First check mapping (aes), then params value = self.mapping.get(aesthetic) or self.params.get(aesthetic) @@ -202,7 +205,12 @@ def resolve_aesthetic(self, aesthetic: str) -> tuple[Any, pd.Series | None, dict color_map = self._create_color_map(series) return value, series, color_map else: - # It's a literal value + # Value is not a column - could be a literal or a typo + # If validation is enabled and value is a string that looks like it could + # be a column name (not a color/known value), provide helpful error + if self.validate and isinstance(value, str) and aesthetic in ('x', 'y', 'group', 'label'): + # For positional aesthetics, a string must be a column reference + self.validate_column(value, aesthetic) return value, None, None def _is_continuous(self, series: pd.Series) -> bool: diff --git a/ggplotly/coords/__init__.py b/ggplotly/coords/__init__.py index bca9bcf..05a8a9a 100644 --- a/ggplotly/coords/__init__.py +++ b/ggplotly/coords/__init__.py @@ -2,8 +2,9 @@ from .coord_base import Coord from .coord_cartesian import coord_cartesian +from .coord_fixed import coord_fixed from .coord_flip import coord_flip from .coord_polar import coord_polar from .coord_sf import coord_sf -__all__ = ["Coord", "coord_flip", "coord_polar", "coord_cartesian", "coord_sf"] +__all__ = ["Coord", "coord_cartesian", "coord_fixed", "coord_flip", "coord_polar", "coord_sf"] diff --git a/ggplotly/coords/coord_fixed.py b/ggplotly/coords/coord_fixed.py new file mode 100644 index 0000000..d4dbb88 --- /dev/null +++ b/ggplotly/coords/coord_fixed.py @@ -0,0 +1,110 @@ +# coords/coord_fixed.py +"""Fixed aspect ratio coordinate system.""" + +from .coord_base import Coord + + +class coord_fixed(Coord): + """ + Cartesian coordinate system with a fixed aspect ratio. + + A fixed aspect ratio ensures that one unit on the x-axis is the same length + as one unit on the y-axis (when ratio=1). This is essential for: + - Maps where distortion must be avoided + - Scatter plots where the relationship shape matters + - Any plot where physical proportions are important + + Parameters: + ratio (float): The aspect ratio, expressed as y/x. Default is 1, meaning + one unit on the x-axis equals one unit on the y-axis. + - ratio > 1: y-axis units are longer than x-axis units + - ratio < 1: x-axis units are longer than y-axis units + xlim (tuple, optional): Two-element tuple (min, max) for x-axis limits. + ylim (tuple, optional): Two-element tuple (min, max) for y-axis limits. + expand (bool): If True (default), add a small expansion to the limits. + clip (str): Should drawing be clipped to extent of plot panel? + Options: 'on' (default) or 'off'. + + Examples: + >>> # Equal aspect ratio (1:1) + >>> ggplot(df, aes(x='x', y='y')) + geom_point() + coord_fixed() + + >>> # Map with equal coordinates + >>> ggplot(map_df, aes(x='lon', y='lat')) + geom_polygon() + coord_fixed() + + >>> # Custom aspect ratio (y is twice as long as x) + >>> ggplot(df, aes(x='x', y='y')) + geom_point() + coord_fixed(ratio=2) + + >>> # Fixed ratio with explicit limits + >>> ggplot(df, aes(x='x', y='y')) + geom_point() + coord_fixed(xlim=(0, 10), ylim=(0, 10)) + + >>> # Circle should look like a circle, not an ellipse + >>> ggplot(circle_df, aes(x='x', y='y')) + geom_path() + coord_fixed() + + See Also: + coord_cartesian: Cartesian coordinates without fixed ratio + coord_sf: Coordinate system for spatial data (also maintains aspect ratio) + """ + + def __init__(self, ratio=1, xlim=None, ylim=None, expand=True, clip='on'): + """ + Initialize the fixed aspect ratio coordinate system. + + Parameters: + ratio (float): Aspect ratio y/x. Default is 1 (equal scaling). + xlim (tuple, optional): X-axis limits (min, max). + ylim (tuple, optional): Y-axis limits (min, max). + expand (bool): Whether to add expansion around limits. Default is True. + clip (str): Clipping mode ('on' or 'off'). Default is 'on'. + """ + self.ratio = ratio + self.xlim = xlim + self.ylim = ylim + self.expand = expand + self.clip = clip + + def _apply_expansion(self, limits, default_expand=(0.05, 0)): + """Apply expansion to limits if expand is True.""" + if limits is None or not self.expand: + return limits + + low, high = limits + data_range = high - low + + mult = default_expand[0] + add = default_expand[1] if len(default_expand) > 1 else 0 + + new_low = low - data_range * mult - add + new_high = high + data_range * mult + add + return [new_low, new_high] + + def apply(self, fig): + """ + Apply fixed aspect ratio to the figure. + + Parameters: + fig (Figure): Plotly figure object. + """ + xaxis_update = {} + yaxis_update = { + "scaleanchor": "x", + "scaleratio": self.ratio, + } + + # Apply limits if specified + if self.xlim is not None: + expanded_xlim = self._apply_expansion(self.xlim) + xaxis_update["range"] = expanded_xlim + + if self.ylim is not None: + expanded_ylim = self._apply_expansion(self.ylim) + yaxis_update["range"] = expanded_ylim + + # Handle clipping + if self.clip == 'off': + for trace in fig.data: + if hasattr(trace, 'cliponaxis'): + trace.cliponaxis = False + + fig.update_xaxes(**xaxis_update) + fig.update_yaxes(**yaxis_update) diff --git a/ggplotly/data_utils.py b/ggplotly/data_utils.py index 9e10750..ea78df3 100644 --- a/ggplotly/data_utils.py +++ b/ggplotly/data_utils.py @@ -72,6 +72,10 @@ def normalize_data(data, mapping: dict[str, Any]) -> tuple[pd.DataFrame | None, mapping = mapping.copy() if mapping else {} index_name = None + # Convert dict to DataFrame + if isinstance(data, dict): + data = pd.DataFrame(data) + # Check for MultiIndex (not supported) if isinstance(data.index, pd.MultiIndex): raise ValueError( diff --git a/ggplotly/geoms/__init__.py b/ggplotly/geoms/__init__.py index 55d5bee..d0125e5 100644 --- a/ggplotly/geoms/__init__.py +++ b/ggplotly/geoms/__init__.py @@ -17,6 +17,7 @@ from .geom_histogram import geom_histogram from .geom_hline import geom_hline from .geom_jitter import geom_jitter +from .geom_label import geom_label from .geom_line import geom_line from .geom_lines import geom_lines from .geom_map import geom_map, geom_sf @@ -28,6 +29,7 @@ from .geom_qq import geom_qq from .geom_qq_line import geom_qq_line from .geom_range import geom_range +from .geom_rect import geom_rect from .geom_ribbon import geom_ribbon from .geom_rug import geom_rug from .geom_sankey import geom_sankey @@ -60,6 +62,8 @@ "geom_ribbon", "geom_tile", "geom_text", + "geom_label", + "geom_rect", "geom_errorbar", "geom_segment", "geom_step", diff --git a/ggplotly/geoms/geom_abline.py b/ggplotly/geoms/geom_abline.py index 23d4261..42cf838 100644 --- a/ggplotly/geoms/geom_abline.py +++ b/ggplotly/geoms/geom_abline.py @@ -8,8 +8,7 @@ class geom_abline(Geom): """Geom for drawing lines with specified slope and intercept.""" - __name__ = "geom_abline" - + required_aes = [] # slope/intercept come from params, not mapping default_params = {"size": 1} def __init__(self, data=None, mapping=None, **params): @@ -52,15 +51,7 @@ def __init__(self, data=None, mapping=None, **params): def _draw_impl(self, fig, data, row, col): - # Get color from params, or use theme default - color = self.params.get("color", None) - if color is None and hasattr(self, 'theme') and self.theme: - import plotly.express as px - palette = self.theme.color_map if hasattr(self.theme, 'color_map') and self.theme.color_map else px.colors.qualitative.Plotly - color = palette[0] - elif color is None: - color = 'black' - + color = self._get_reference_line_color(default='black') linetype = self.params.get("linetype", "solid") alpha = self.params.get("alpha", 1) line_width = self.params.get("size", 1) diff --git a/ggplotly/geoms/geom_acf.py b/ggplotly/geoms/geom_acf.py index 2cd1d4a..87b937c 100644 --- a/ggplotly/geoms/geom_acf.py +++ b/ggplotly/geoms/geom_acf.py @@ -44,6 +44,8 @@ class geom_acf(Geom): >>> ggplot(df, aes(y='value')) + geom_acf(color='coral', nlags=30) """ + required_aes = ['y'] + default_params = { "nlags": 40, "alpha": 0.05, diff --git a/ggplotly/geoms/geom_area.py b/ggplotly/geoms/geom_area.py index c6add20..2dfc481 100644 --- a/ggplotly/geoms/geom_area.py +++ b/ggplotly/geoms/geom_area.py @@ -15,19 +15,29 @@ class geom_area(Geom): Parameters: mapping (aes): Aesthetic mappings created by aes(). - color (str, optional): Color of the area. If a categorical variable is mapped to color, different colors will be assigned. + color (str, optional): Color of the area outline. + colour (str, optional): Alias for color (British spelling). linetype (str, optional): Line type ('solid', 'dash', etc.). Default is 'solid'. + size (float, optional): Line width. Default is 1. + linewidth (float, optional): Alias for size (ggplot2 3.4+ compatibility). group (str, optional): Grouping variable for the areas. - fill (str, optional): Fill color for the area. Default is 'lightblue'. + fill (str, optional): Fill color for the area. alpha (float, optional): Transparency level for the fill color. Default is 0.5. - showlegend (bool, optional): Whether to show legend entries. Default is True. + position (str, optional): Position adjustment. Options: + - 'identity': No stacking (default) + - 'stack': Stack areas on top of each other + show_legend (bool, optional): Whether to show legend entries. Default is True. + showlegend (bool, optional): Alias for show_legend. + na_rm (bool, optional): If True, remove missing values. Default is False. Examples: >>> ggplot(df, aes(x='x', y='y')) + geom_area() >>> ggplot(df, aes(x='x', y='y', fill='group')) + geom_area(alpha=0.5) + >>> ggplot(df, aes(x='x', y='y', fill='group')) + geom_area(position='stack') """ - default_params = {"size": 1} + required_aes = ['x', 'y'] + default_params = {"size": 1, "alpha": 0.5, "position": "identity"} def _draw_impl(self, fig, data, row, col): """ @@ -48,14 +58,25 @@ def _draw_impl(self, fig, data, row, col): if "size" in self.mapping: del self.mapping["size"] + # Handle position parameter for stacking + position = self.params.get("position", "identity") + plot = go.Scatter payload = dict( mode="lines", - fill="tozeroy", line_dash=self.params.get("linetype", "solid"), name=self.params.get("name", "Area"), ) + # Set fill mode based on position + if position == "stack": + # Use stackgroup for stacked areas + payload["stackgroup"] = "one" + payload["fill"] = "tonexty" + else: + # Default: fill to zero + payload["fill"] = "tozeroy" + color_targets = dict( # fill="line", fill="fillcolor", diff --git a/ggplotly/geoms/geom_bar.py b/ggplotly/geoms/geom_bar.py index 0add0fe..ad2fe1d 100644 --- a/ggplotly/geoms/geom_bar.py +++ b/ggplotly/geoms/geom_bar.py @@ -35,6 +35,8 @@ class geom_bar(Geom): >>> ggplot(df, aes(x='category')) + geom_bar(width=0.5) # narrower bars """ + required_aes = ['x'] # y is computed by stat_count + def _apply_stats(self, data): """Add default stat_count if no stats and stat='count'.""" if self.stats == []: diff --git a/ggplotly/geoms/geom_base.py b/ggplotly/geoms/geom_base.py index c41c59c..f3b28ff 100644 --- a/ggplotly/geoms/geom_base.py +++ b/ggplotly/geoms/geom_base.py @@ -2,9 +2,68 @@ from ..aes import aes from ..aesthetic_mapper import AestheticMapper +from ..exceptions import ColumnNotFoundError, RequiredAestheticError from ..trace_builders import get_trace_builder +# CSS named colors for validation (avoids mistaking column names for colors) +CSS_COLORS = frozenset({ + 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', + 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', + 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', + 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', + 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', + 'darkgrey', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', + 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', + 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', + 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', + 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', + 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', + 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', + 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', + 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', + 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', + 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', + 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', + 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', + 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', + 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', + 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', + 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', + 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', + 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', + 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', + 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', + 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', + 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', + 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', + 'yellowgreen' +}) + + +def is_valid_color(value): + """ + Check if a value looks like a valid color (not a column name). + + Parameters: + value: The value to check + + Returns: + bool: True if value appears to be a valid CSS color + """ + if value is None: + return False + if not isinstance(value, str): + return False + # Check for common color formats + if value.startswith('#'): + return True + if value.startswith('rgb') or value.startswith('hsl'): + return True + # Check against CSS color names + return value.lower() in CSS_COLORS + + class Geom: """ Base class for all geoms (geometric objects). @@ -17,16 +76,27 @@ class Geom: data (DataFrame, optional): Data to use for this geom. If None, uses the data from the ggplot object. mapping (aes, optional): Aesthetic mappings for this geom. + na_rm (bool): If True, silently remove missing values. Default False. + show_legend (bool): Whether to show this geom in the legend. Default True. **params: Additional parameters passed to the geom. Examples: >>> ggplot(df, aes(x='x', y='y')) + geom_point() >>> ggplot(df, aes(x='x', y='y')) + geom_point(color='red', size=3) + >>> ggplot(df, aes(x='x', y='y')) + geom_point(na_rm=True) """ # Default parameters for this geom. Subclasses should override this. + # Note: na_rm and show_legend are always added via base_defaults in __init__ default_params: dict = {} + # Required aesthetics for this geom. Subclasses should override. + # Example: required_aes = ['x', 'y'] for geom_point + required_aes: list = [] + + # Optional aesthetics that can be mapped to columns + optional_aes: list = ['color', 'fill', 'size', 'alpha', 'shape', 'group'] + def __init__(self, data=None, mapping=None, **params): """ Initialize the geom. @@ -44,8 +114,27 @@ def __init__(self, data=None, mapping=None, **params): self.data = data self.mapping = mapping.mapping if mapping else {} - # Merge default params with user-provided params (user params take precedence) - self.params = {**self.default_params, **params} + # Merge base class defaults, subclass defaults, and user-provided params + # Base class defaults for na_rm, show_legend + base_defaults = {"na_rm": False, "show_legend": True} + self.params = {**base_defaults, **self.default_params, **params} + + # Handle parameter aliases for ggplot2 compatibility + # linewidth is an alias for size (line width in ggplot2 3.4+) + if "linewidth" in self.params and "size" not in params: + self.params["size"] = self.params["linewidth"] + + # showlegend is an alias for show_legend (Plotly convention) + if "showlegend" in self.params and "show_legend" not in params: + self.params["show_legend"] = self.params["showlegend"] + + # colour is an alias for color (British spelling) + if "colour" in self.params and "color" not in params: + self.params["color"] = self.params["colour"] + + # na.rm style can be passed as na_rm (Python convention) + # Already handled by default, but normalize any variants + self.stats = [] self.layers = [] # Track whether this geom has explicit data or inherited from plot @@ -54,14 +143,6 @@ def __init__(self, data=None, mapping=None, **params): self._global_color_map = None self._global_shape_map = None - # def __add__(self, other): - # if isinstance(other, Geom): - # self.layers.append(other) - - # return self.copy() - # else: - # raise ValueError("Only Geom and Stat objects can be added to Geom objects.") - def copy(self): """ Create a deep copy of this geom. @@ -84,11 +165,78 @@ def setup_data(self, data, plot_mapping): Returns: None: Modifies the geom in place. """ + # Store original geom mapping before merging (for validation filtering) + self._original_mapping = self.mapping.copy() + # Merge plot mapping and geom mapping, with geom mapping taking precedence combined_mapping = {**plot_mapping, **self.mapping} self.mapping = combined_mapping self.data = data.copy() + def validate_required_aesthetics(self, data=None): + """ + Validate that required aesthetics are present and reference valid columns. + + This method checks: + 1. All required aesthetics are present in the mapping + 2. Mapped columns actually exist in the data + + Parameters: + data (DataFrame, optional): Data to validate against. Uses self.data if not provided. + + Raises: + RequiredAestheticError: If required aesthetics are missing + ColumnNotFoundError: If mapped columns don't exist in data + """ + data = data if data is not None else self.data + + # Check required aesthetics are present + missing = [] + for aes_name in self.required_aes: + if aes_name not in self.mapping: + missing.append(aes_name) + + if missing: + geom_name = self.__class__.__name__ + raise RequiredAestheticError(geom_name, missing) + + # Validate that mapped columns exist in data + if data is not None and not data.empty: + columns = frozenset(data.columns) + all_aes = self.required_aes + self.optional_aes + + # Get original mapping to identify inherited vs explicit aesthetics + original_mapping = getattr(self, '_original_mapping', {}) + + for aes_name in all_aes: + value = self.mapping.get(aes_name) + if value is not None and isinstance(value, str): + # Skip validation for inherited aesthetics when geom has explicit data + # (inherited aesthetics reference columns in the global data, not geom's data) + if self._has_explicit_data and aes_name not in original_mapping: + continue + + # Check if it's supposed to be a column reference + if aes_name in ('x', 'y', 'xend', 'yend', 'xmin', 'xmax', 'ymin', 'ymax', + 'label', 'group', 'weight'): + # These are always column references + if value not in columns: + raise ColumnNotFoundError(value, list(data.columns), aes_name) + elif aes_name in ('color', 'fill', 'size', 'shape', 'alpha'): + # These could be literal values or column references + # Only validate if it looks like a column reference (not a color name, etc.) + if value in columns: + pass # Valid column reference + elif not is_valid_color(value) and value not in columns: + # It's a string but not a color and not a column - might be a typo + # Only raise error if it's plausibly a column name (no spaces, etc.) + if ' ' not in value and not value.startswith('#'): + # Could be a typo - check for similar columns + from difflib import get_close_matches + similar = get_close_matches(value, list(columns), n=1, cutoff=0.6) + if similar: + raise ColumnNotFoundError(value, list(data.columns), aes_name) + def draw(self, fig, data=None, row=1, col=1): """ Draw the geometry on the figure. @@ -104,15 +252,52 @@ def draw(self, fig, data=None, row=1, col=1): Returns: None: Modifies the figure in place. + + Raises: + RequiredAestheticError: If required aesthetics are missing + ColumnNotFoundError: If mapped columns don't exist in data """ data = data if data is not None else self.data + # Handle na_rm: remove rows with missing values in mapped columns + if self.params.get("na_rm", False): + data = self._remove_missing(data) + # Apply any stats to transform the data + # Stats may add aesthetics (e.g., stat_ecdf adds 'y') data = self._apply_stats(data) + # Validate required aesthetics and column references AFTER stats + # (stats may provide required aesthetics like 'y' from stat_ecdf) + if self.required_aes: + self.validate_required_aesthetics(data) + # Delegate to subclass implementation self._draw_impl(fig, data, row, col) + def _remove_missing(self, data): + """ + Remove rows with missing values in mapped columns. + + Parameters: + data (DataFrame): Input data. + + Returns: + DataFrame: Data with missing values removed from mapped columns. + """ + if data is None or data.empty: + return data + + # Get columns that are mapped to aesthetics + mapped_cols = [] + for col in self.mapping.values(): + if isinstance(col, str) and col in data.columns: + mapped_cols.append(col) + + if mapped_cols: + return data.dropna(subset=mapped_cols) + return data + def _apply_stats(self, data): """ Apply all attached stats to transform the data. @@ -127,6 +312,35 @@ def _apply_stats(self, data): data, self.mapping = stat.compute(data) return data + def _get_reference_line_color(self, default='#1f77b4'): + """ + Get color for reference lines (vline, hline, abline). + + Resolves color from params, theme palette, or default value. + + Parameters: + default (str): Default color if no other source available. + + Returns: + str: Color string for the reference line. + """ + color = self.params.get("color", None) + if color is not None: + return color + + # Try theme palette + if hasattr(self, 'theme') and self.theme: + if hasattr(self.theme, 'color_map') and self.theme.color_map: + return self.theme.color_map[0] + # Try Plotly's default palette + try: + import plotly.express as px + return px.colors.qualitative.Plotly[0] + except (ImportError, IndexError): + pass + + return default + def _draw_impl(self, fig, data, row, col): """ Implementation of the actual drawing logic. @@ -180,54 +394,6 @@ def _apply_color_targets(self, target_props: dict, style_props: dict, value_key= """ result = {} - def is_valid_color(value): - """Check if a value looks like a valid color (not a column name).""" - if value is None: - return False - if not isinstance(value, str): - return False - # Check for common color formats - if value.startswith('#'): - return True - if value.startswith('rgb') or value.startswith('hsl'): - return True - # Check against a list of known CSS color names - # This is safer than heuristics since column names like 'group', 'species', etc. - # could otherwise be mistaken for colors - css_colors = { - 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', - 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', - 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', - 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', - 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', - 'darkgrey', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', - 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', - 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', - 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', - 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', - 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', - 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', - 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', - 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', - 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', - 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', - 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', - 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', - 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', - 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', - 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', - 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', - 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', - 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', - 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', - 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', - 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', - 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', - 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', - 'yellowgreen' - } - return value.lower() in css_colors - # Determine the color to use if value_key is not None: # Looking up color for a specific category diff --git a/ggplotly/geoms/geom_boxplot.py b/ggplotly/geoms/geom_boxplot.py index 6ce67de..7dc6f89 100644 --- a/ggplotly/geoms/geom_boxplot.py +++ b/ggplotly/geoms/geom_boxplot.py @@ -8,6 +8,8 @@ class geom_boxplot(Geom): """Geom for drawing boxplots.""" + required_aes = ['x', 'y'] + def __init__(self, data=None, mapping=None, outlier_colour=None, outlier_color=None, outlier_fill=None, outlier_shape='circle', outlier_size=1.5, outlier_stroke=0.5, outlier_alpha=None, notch=False, varwidth=False, diff --git a/ggplotly/geoms/geom_candlestick.py b/ggplotly/geoms/geom_candlestick.py index 8a2142a..7a123a0 100644 --- a/ggplotly/geoms/geom_candlestick.py +++ b/ggplotly/geoms/geom_candlestick.py @@ -8,6 +8,8 @@ class geom_candlestick(Geom): """Geom for drawing candlestick charts for financial OHLC data.""" + required_aes = ['x', 'open', 'high', 'low', 'close'] + def __init__(self, data=None, mapping=None, **params): """ Create a candlestick chart for financial data. @@ -129,6 +131,8 @@ def _draw_impl(self, fig, data, row, col): class geom_ohlc(Geom): """Geom for drawing OHLC (Open-High-Low-Close) bar charts.""" + required_aes = ['x', 'open', 'high', 'low', 'close'] + def __init__(self, data=None, mapping=None, **params): """ Draw OHLC bar charts for financial data. diff --git a/ggplotly/geoms/geom_col.py b/ggplotly/geoms/geom_col.py index 7cf147f..f144375 100644 --- a/ggplotly/geoms/geom_col.py +++ b/ggplotly/geoms/geom_col.py @@ -14,14 +14,23 @@ class geom_col(Geom): Parameters: fill (str, optional): Fill color for the columns. + color (str, optional): Border color for the columns. alpha (float, optional): Transparency level for the fill color. Default is 1. + width (float, optional): Width of the bars as fraction of available space. + Default is 0.9. Values should be between 0 and 1. group (str, optional): Grouping variable for the columns. + na_rm (bool, optional): If True, remove missing values. Default is False. + show_legend (bool, optional): Whether to show in legend. Default is True. Examples: >>> ggplot(df, aes(x='category', y='value')) + geom_col() >>> ggplot(df, aes(x='category', y='value', fill='group')) + geom_col() + >>> ggplot(df, aes(x='category', y='value')) + geom_col(width=0.5) """ + required_aes = ['x', 'y'] + default_params = {"alpha": 1, "width": 0.9} + def _draw_impl(self, fig, data, row, col): """ Draw column(s) on the figure. @@ -39,6 +48,10 @@ def _draw_impl(self, fig, data, row, col): payload = dict() payload["name"] = self.params.get("name", "Column") + # Apply width parameter for bar width + width = self.params.get("width", 0.9) + payload["width"] = width + # Note: opacity/alpha is handled by _transform_fig via AestheticMapper # Don't add it to payload to avoid duplicate keyword argument diff --git a/ggplotly/geoms/geom_contour.py b/ggplotly/geoms/geom_contour.py index 7d82b73..f8a060b 100644 --- a/ggplotly/geoms/geom_contour.py +++ b/ggplotly/geoms/geom_contour.py @@ -9,6 +9,7 @@ class geom_contour(Geom): """Geom for drawing contour lines from 2D data.""" + required_aes = ['x', 'y'] # z is optional (computed from KDE if not provided) default_params = {"size": 1} def __init__(self, data=None, mapping=None, **params): diff --git a/ggplotly/geoms/geom_contour_filled.py b/ggplotly/geoms/geom_contour_filled.py index 1e8b975..7d03e2b 100644 --- a/ggplotly/geoms/geom_contour_filled.py +++ b/ggplotly/geoms/geom_contour_filled.py @@ -9,6 +9,7 @@ class geom_contour_filled(Geom): """Geom for drawing filled contours from 2D data.""" + required_aes = ['x', 'y'] # z is optional (computed from KDE if not provided) default_params = {"alpha": 0.8} def __init__(self, data=None, mapping=None, **params): diff --git a/ggplotly/geoms/geom_density.py b/ggplotly/geoms/geom_density.py index 07c9a7a..49be861 100644 --- a/ggplotly/geoms/geom_density.py +++ b/ggplotly/geoms/geom_density.py @@ -11,6 +11,7 @@ class geom_density(Geom): """Geom for drawing density plots.""" + required_aes = ['x'] # y is computed by KDE default_params = {"size": 2} def __init__(self, data=None, mapping=None, bw='nrd0', adjust=1, kernel='gaussian', @@ -48,9 +49,14 @@ def __init__(self, data=None, mapping=None, bw='nrd0', adjust=1, kernel='gaussia Additional parameters including: - fill (str): Fill color for the density plot. + - color (str): Line color for the density curve. + - colour (str): Alias for color (British spelling). - alpha (float): Transparency level. Default is 0.5. + - size (float): Line width. Default is 2. + - linewidth (float): Alias for size (ggplot2 3.4+ compatibility). - linetype (str): Line style ('solid', 'dash', etc.). - na_rm (bool): If True, silently remove missing values. + - show_legend (bool): Whether to show in legend. Default is True. Examples -------- diff --git a/ggplotly/geoms/geom_edgebundle.py b/ggplotly/geoms/geom_edgebundle.py index 435b321..e52c74b 100644 --- a/ggplotly/geoms/geom_edgebundle.py +++ b/ggplotly/geoms/geom_edgebundle.py @@ -83,6 +83,8 @@ def _extract_graph_data(graph, weight_attr=None): class geom_edgebundle(Geom): """Bundled edges for graph visualization using force-directed edge bundling.""" + required_aes = ['x', 'y', 'xend', 'yend'] # Not needed when using graph parameter + def __init__( self, mapping=None, @@ -210,6 +212,10 @@ def __init__( edges_df, nodes_df = _extract_graph_data(graph, weight_attr=weight) self.data = edges_df self.nodes = nodes_df + # Set up mapping for graph-extracted data + self.mapping = {'x': 'x', 'y': 'y', 'xend': 'xend', 'yend': 'yend'} + if 'weight' in edges_df.columns: + self.mapping['weight'] = 'weight' # Bundling parameters self.K = K diff --git a/ggplotly/geoms/geom_errorbar.py b/ggplotly/geoms/geom_errorbar.py index 5034bd9..d8fc103 100644 --- a/ggplotly/geoms/geom_errorbar.py +++ b/ggplotly/geoms/geom_errorbar.py @@ -18,10 +18,12 @@ class geom_errorbar(Geom): color (str, optional): Color of the error bars. alpha (float, optional): Transparency level for the error bars. Default is 1. linetype (str, optional): Line style of the error bars ('solid', 'dash', etc.). + width (float, optional): Width of the error bar caps in pixels. Default is 4. group (str, optional): Grouping variable for the error bars. """ - default_params = {"size": 2} + required_aes = ['x', 'y'] # ymin/ymax or yerr are also needed but handled in _draw_impl + default_params = {"size": 2, "width": 4} def _draw_impl(self, fig, data, row, col): style_props = self._get_style_props(data) @@ -39,6 +41,7 @@ def _draw_impl(self, fig, data, row, col): ymax = data[self.mapping["ymax"]] linetype = self.params.get("linetype", "solid") + width = self.params.get("width", 4) alpha = style_props['alpha'] group_values = style_props['group_series'] @@ -63,6 +66,7 @@ def _draw_impl(self, fig, data, row, col): type="data", array=ymax[group_mask] - y[group_mask], arrayminus=y[group_mask] - ymin[group_mask], + width=width, ), mode="markers", line_dash=linetype, @@ -91,6 +95,7 @@ def _draw_impl(self, fig, data, row, col): type="data", array=ymax[cat_mask] - y[cat_mask], arrayminus=y[cat_mask] - ymin[cat_mask], + width=width, ), mode="markers", line_dash=linetype, @@ -113,6 +118,7 @@ def _draw_impl(self, fig, data, row, col): type="data", array=ymax - y, arrayminus=y - ymin, + width=width, ), mode="markers", line_dash=linetype, diff --git a/ggplotly/geoms/geom_fanchart.py b/ggplotly/geoms/geom_fanchart.py index 9f7cde4..7c3728d 100644 --- a/ggplotly/geoms/geom_fanchart.py +++ b/ggplotly/geoms/geom_fanchart.py @@ -50,6 +50,8 @@ class geom_fanchart(Geom): >>> ggplot(df) + geom_fanchart(color='coral', alpha=0.3) """ + required_aes = [] # Flexible - uses columns from DataFrame, x is optional + default_params = { "percentiles": [10, 25, 50, 75, 90], "color": "steelblue", diff --git a/ggplotly/geoms/geom_histogram.py b/ggplotly/geoms/geom_histogram.py index be73e78..8dba6e4 100644 --- a/ggplotly/geoms/geom_histogram.py +++ b/ggplotly/geoms/geom_histogram.py @@ -38,6 +38,8 @@ class geom_histogram(Geom): >>> ggplot(df, aes(x='value', fill='category')) + geom_histogram(alpha=0.5) """ + required_aes = ['x'] # y is computed by stat_bin + def __init__(self, data=None, mapping=None, bins=30, binwidth=None, boundary=None, center=None, barmode="stack", bin=None, **params): """ @@ -72,7 +74,10 @@ def _apply_stats(self, data): Handles grouping by fill/color/group aesthetics automatically. """ na_rm = self.params.get("na_rm", False) - x_col = self.mapping.get("x") + # Store original x column name (mapping may have been modified by previous facet panel) + if not hasattr(self, '_original_x_col'): + self._original_x_col = self.mapping.get("x") + x_col = self._original_x_col # Determine grouping column from fill, color, or group mapping group_col = None @@ -95,11 +100,43 @@ def _apply_stats(self, data): # Compute bins - either grouped or ungrouped if group_col is not None: - # Grouped histogram: compute separate bins for each group + # Grouped histogram: compute bins using SAME edges for all groups + # First, compute bin edges from ALL data so groups align for stacking + import numpy as np + all_x = data[x_col].dropna().values + x_min, x_max = np.nanmin(all_x), np.nanmax(all_x) + x_range = x_max - x_min + + # Determine bin width + if self.binwidth is not None: + width = self.binwidth + else: + width = x_range / self.bins + + # Compute shared bin edges + if self.boundary is not None: + shift = (x_min - self.boundary) % width + bin_min = x_min - shift + elif self.center is not None: + shift = (x_min - self.center + width / 2) % width + bin_min = x_min - shift + else: + bin_min = x_min + + n_bins = int(np.ceil((x_max - bin_min) / width)) + 1 + shared_breaks = bin_min + np.arange(n_bins + 1) * width + + # Now compute bins for each group using the shared breaks binned_frames = [] for group_value in data[group_col].unique(): group_data = data[data[group_col] == group_value].copy() - binned_data = bin_stat.compute(group_data) + # Create a new stat_bin with the shared breaks + group_bin_stat = stat_bin( + mapping={'x': x_col}, + breaks=shared_breaks, + na_rm=na_rm + ) + binned_data = group_bin_stat.compute(group_data) binned_data[group_col] = group_value binned_frames.append(binned_data) @@ -170,23 +207,80 @@ def _draw_impl(self, fig, data, row, col): Returns: None: Modifies the figure in place. """ - payload = dict() - payload["name"] = self.params.get("name", "Histogram") + # Get style properties for color mapping + style_props = self._get_style_props(data) + alpha = style_props['alpha'] - # Use Bar trace with width from stat_bin - plot = go.Bar + # Determine grouping column from fill, color, or group mapping + group_col = None + for aesthetic in ['fill', 'color', 'group']: + if aesthetic in self.mapping: + potential_col = self.mapping[aesthetic] + if potential_col in data.columns: + group_col = potential_col + break - color_targets = dict( - color="marker_color", - ) + # Get x and y column names from mapping + x_col = self.mapping.get("x", "x") + y_col = self.mapping.get("y", "count") - self._transform_fig( - plot, - fig, - data, - payload, - color_targets, - row, - col, - barmode=self.barmode, - ) + # Initialize legend tracking on figure if not present + if not hasattr(fig, '_ggplotly_shown_legendgroups'): + fig._ggplotly_shown_legendgroups = set() + + if group_col is not None: + # Grouped histogram - one trace per group with proper width + cat_map = style_props.get('color_map') or style_props.get('fill_map', {}) + if not cat_map: + # Build a color map from unique values + import plotly.express as px + unique_vals = data[group_col].unique() + colors = px.colors.qualitative.Plotly + cat_map = {val: colors[i % len(colors)] for i, val in enumerate(unique_vals)} + + for cat_value in cat_map.keys(): + cat_mask = data[group_col] == cat_value + if not cat_mask.any(): + continue + + subset = data[cat_mask] + legend_name = str(cat_value) + + # Check if we should show this legend entry + show_legend = legend_name not in fig._ggplotly_shown_legendgroups + if show_legend: + fig._ggplotly_shown_legendgroups.add(legend_name) + + fig.add_trace( + go.Bar( + x=subset[x_col], + y=subset[y_col], + width=subset['width'] if 'width' in subset.columns else None, + marker_color=cat_map.get(cat_value, style_props['default_color']), + opacity=alpha, + name=legend_name, + showlegend=show_legend, + legendgroup=legend_name, + ), + row=row, + col=col, + ) + else: + # Single histogram - one trace + color = style_props.get('color') or style_props.get('fill') or style_props['default_color'] + fig.add_trace( + go.Bar( + x=data[x_col], + y=data[y_col], + width=data['width'] if 'width' in data.columns else None, + marker_color=color, + opacity=alpha, + name=self.params.get("name", "Histogram"), + showlegend=self.params.get("showlegend", True), + ), + row=row, + col=col, + ) + + fig.update_yaxes(rangemode="tozero") + fig.update_layout(barmode=self.barmode) diff --git a/ggplotly/geoms/geom_hline.py b/ggplotly/geoms/geom_hline.py index 47195e6..1474edd 100644 --- a/ggplotly/geoms/geom_hline.py +++ b/ggplotly/geoms/geom_hline.py @@ -5,8 +5,7 @@ class geom_hline(Geom): """Geom for drawing horizontal reference lines.""" - __name__ = "geom_hline" - + required_aes = [] # yintercept comes from data/params, not mapping default_params = {"size": 2} def __init__(self, data=None, mapping=None, **params): @@ -55,17 +54,7 @@ def _draw_impl(self, fig, data, row, col): if not isinstance(y, list): y = [y] - # Get color from params, or use theme default - color = self.params.get("color", None) - if color is None and hasattr(self, 'theme') and self.theme: - # Use first color from theme palette - import plotly.express as px - palette = self.theme.color_map if hasattr(self.theme, 'color_map') and self.theme.color_map else px.colors.qualitative.Plotly - color = palette[0] - elif color is None: - # Default to theme's default color - color = '#1f77b4' - + color = self._get_reference_line_color() linetype = self.params.get("linetype", "solid") alpha = self.params.get("alpha", 1) name = self.params.get("name", "hline") diff --git a/ggplotly/geoms/geom_jitter.py b/ggplotly/geoms/geom_jitter.py index 6e96571..8857023 100644 --- a/ggplotly/geoms/geom_jitter.py +++ b/ggplotly/geoms/geom_jitter.py @@ -30,6 +30,7 @@ class geom_jitter(Geom): >>> ggplot(df, aes(x='category', y='value', color='group')) + geom_jitter(width=0.3) """ + required_aes = ['x', 'y'] default_params = {"size": 8} def __init__(self, data=None, mapping=None, **params): diff --git a/ggplotly/geoms/geom_label.py b/ggplotly/geoms/geom_label.py new file mode 100644 index 0000000..573c3eb --- /dev/null +++ b/ggplotly/geoms/geom_label.py @@ -0,0 +1,258 @@ +# geoms/geom_label.py + +import plotly.graph_objects as go + +from ..aesthetic_mapper import AestheticMapper +from .geom_base import Geom + + +class geom_label(Geom): + """ + Geom for adding text labels with a background box. + + Similar to geom_text but draws a rectangle behind the text for better + readability, especially when labels overlap with data. + + Parameters: + hjust (float, optional): Horizontal justification (0=left, 0.5=center, 1=right). + Default is 0.5 (centered). + vjust (float, optional): Vertical justification (0=bottom, 0.5=middle, 1=top). + Default is 0.5 (middle). + nudge_x (float, optional): Horizontal offset to apply to label position. Default is 0. + nudge_y (float, optional): Vertical offset to apply to label position. Default is 0. + size (float, optional): Text size in points. Default is 11. + family (str, optional): Font family. Default is None (use Plotly default). + fontface (str, optional): Font face ('plain', 'bold', 'italic', 'bold.italic'). + Default is 'plain'. + color (str, optional): Text color. Default is 'black'. + fill (str, optional): Background fill color. Default is 'white'. + alpha (float, optional): Background transparency (0-1). Default is 0.8. + label_padding (float, optional): Padding around text in pixels. Default is 4. + label_r (float, optional): Border radius in pixels. Default is 2. + label_size (float, optional): Border line width. Default is 0.5. + parse (bool, optional): If True, parse text labels as LaTeX math expressions. + Default is False. + na_rm (bool, optional): If True, silently remove missing values. Default is False. + + Aesthetics: + - x: x-axis position (required) + - y: y-axis position (required) + - label: Text to display (required) + - color: Text color (optional) + - fill: Background color (optional) + - group: Grouping variable (optional) + + Examples: + >>> # Basic label + >>> ggplot(df, aes(x='x', y='y', label='name')) + geom_label() + + >>> # Styled label + >>> ggplot(df, aes(x='x', y='y', label='name')) + geom_label( + ... fill='lightblue', color='navy', size=12 + ... ) + + >>> # Labels with nudge to avoid overlapping points + >>> (ggplot(df, aes(x='x', y='y', label='name')) + ... + geom_point() + ... + geom_label(nudge_y=0.5)) + + >>> # Labels colored by category + >>> ggplot(df, aes(x='x', y='y', label='name', fill='category')) + geom_label() + + See Also: + geom_text: Text labels without background + """ + + required_aes = ['x', 'y', 'label'] + + default_params = { + "size": 11, + "alpha": 0.8, + "label_padding": 4, + "label_r": 2, + "label_size": 0.5, + "hjust": 0.5, + "vjust": 0.5, + "fill": "white", + "color": "black", + } + + def _hjust_vjust_to_anchor(self, hjust, vjust): + """ + Convert hjust/vjust values to Plotly xanchor/yanchor strings. + + Parameters: + hjust (float): Horizontal justification (0=left, 0.5=center, 1=right) + vjust (float): Vertical justification (0=bottom, 0.5=middle, 1=top) + + Returns: + tuple: (xanchor, yanchor) strings for Plotly annotation + """ + # Determine horizontal anchor + if hjust <= 0.25: + xanchor = "left" + elif hjust >= 0.75: + xanchor = "right" + else: + xanchor = "center" + + # Determine vertical anchor + if vjust <= 0.25: + yanchor = "bottom" + elif vjust >= 0.75: + yanchor = "top" + else: + yanchor = "middle" + + return xanchor, yanchor + + def _fontface_to_plotly(self, fontface): + """Convert R-style fontface to Plotly font properties.""" + if fontface == "bold": + return {"weight": "bold"} + elif fontface == "italic": + return {"style": "italic"} + elif fontface == "bold.italic": + return {"weight": "bold", "style": "italic"} + return {} # plain + + def _draw_impl(self, fig, data, row, col): + """ + Draw text labels with background boxes on the figure. + + Parameters: + fig (Figure): Plotly figure object. + data (DataFrame): Data (already transformed by stats). + row (int): Row position in subplot. + col (int): Column position in subplot. + + Returns: + None: Modifies the figure in place. + """ + # Handle na_rm parameter + na_rm = self.params.get("na_rm", False) + if na_rm: + cols_to_check = [self.mapping["x"], self.mapping["y"], self.mapping["label"]] + data = data.dropna(subset=cols_to_check) + + if len(data) == 0: + return + + # Create aesthetic mapper for this geom + mapper = AestheticMapper(data, self.mapping, self.params, self.theme) + style_props = mapper.get_style_properties() + + x = data[self.mapping["x"]].copy() + y = data[self.mapping["y"]].copy() + label = data[self.mapping["label"]] + + # Handle parse parameter - wrap labels in $...$ for MathJax rendering + parse = self.params.get("parse", False) + if parse: + label = label.apply(lambda t: f"${t}$" if not str(t).startswith("$") else t) + + # Apply nudge offsets + nudge_x = self.params.get("nudge_x", 0) + nudge_y = self.params.get("nudge_y", 0) + if nudge_x != 0: + x = x + nudge_x + if nudge_y != 0: + y = y + nudge_y + + # Get justification + hjust = self.params.get("hjust", 0.5) + vjust = self.params.get("vjust", 0.5) + xanchor, yanchor = self._hjust_vjust_to_anchor(hjust, vjust) + + # Get text styling parameters + font_size = self.params.get("size", 11) + font_family = self.params.get("family", None) + fontface = self.params.get("fontface", "plain") + font_props = self._fontface_to_plotly(fontface) + + # Get label box styling + label_padding = self.params.get("label_padding", 4) + label_r = self.params.get("label_r", 2) + label_size = self.params.get("label_size", 0.5) + alpha = style_props["alpha"] + + # Default colors + default_text_color = self.params.get("color", "black") + default_fill_color = self.params.get("fill", "white") + + # Determine axis references for subplots + # For faceted plots, we need the correct axis reference + xref = f"x{col}" if col > 1 else "x" + yref = f"y{row}" if row > 1 else "y" + # Adjust for subplot layout + if row > 1 or col > 1: + subplot_idx = (row - 1) * fig._grid_ref[0].__len__() + col if hasattr(fig, '_grid_ref') else 1 + if subplot_idx > 1: + xref = f"x{subplot_idx}" + yref = f"y{subplot_idx}" + + # Handle fill mapping for background colors + fill_series = style_props.get("fill_series") + fill_map = style_props.get("fill_map", {}) + fill_col = style_props.get("fill") + + # Handle color mapping for text colors + color_series = style_props.get("color_series") + color_map = style_props.get("color_map", {}) + color_col = style_props.get("color") + + # Add annotations for each label + for idx in data.index: + # Determine fill color for this label + if fill_series is not None and fill_col in data.columns: + cat_value = data.loc[idx, fill_col] + fill_color = fill_map.get(cat_value, default_fill_color) + else: + fill_color = default_fill_color + + # Determine text color for this label + if color_series is not None and color_col in data.columns: + cat_value = data.loc[idx, color_col] + text_color = color_map.get(cat_value, default_text_color) + else: + text_color = default_text_color + + # Build font dict + font_dict = {"size": font_size, "color": text_color} + if font_family: + font_dict["family"] = font_family + font_dict.update(font_props) + + # Add annotation with background + fig.add_annotation( + x=x[idx], + y=y[idx], + text=str(label[idx]), + showarrow=False, + font=font_dict, + xanchor=xanchor, + yanchor=yanchor, + bgcolor=fill_color, + opacity=alpha, + bordercolor=text_color, + borderwidth=label_size, + borderpad=label_padding, + xref=xref, + yref=yref, + ) + + # Add invisible scatter trace for legend if fill is mapped + if fill_series is not None: + for cat_value, color in fill_map.items(): + fig.add_trace( + go.Scatter( + x=[None], + y=[None], + mode="markers", + marker=dict(size=10, color=color, symbol="square"), + name=str(cat_value), + showlegend=True, + ), + row=row, + col=col, + ) diff --git a/ggplotly/geoms/geom_line.py b/ggplotly/geoms/geom_line.py index f78b68f..e05f6a0 100644 --- a/ggplotly/geoms/geom_line.py +++ b/ggplotly/geoms/geom_line.py @@ -17,6 +17,7 @@ class geom_line(Geom): linetype (str, optional): Line style ('solid', 'dash', etc.). Default is 'solid'. alpha (float, optional): Transparency level for the lines. Default is 1. size (float, optional): Line width. Default is 2. + linewidth (float, optional): Alias for size (ggplot2 3.4+ compatibility). group (str, optional): Grouping variable for the lines. Aesthetics: @@ -29,8 +30,7 @@ class geom_line(Geom): geom_path: Connect points in data order (no sorting) """ - __name__ = "geom_line" - + required_aes = ['x', 'y'] default_params = {"size": 2} def _draw_impl(self, fig, data, row, col): diff --git a/ggplotly/geoms/geom_lines.py b/ggplotly/geoms/geom_lines.py index 646c91d..287807c 100644 --- a/ggplotly/geoms/geom_lines.py +++ b/ggplotly/geoms/geom_lines.py @@ -49,6 +49,8 @@ class geom_lines(Geom): >>> ggplot(df) + geom_lines(multicolor=True, palette='Viridis') """ + required_aes = [] # Flexible - uses columns from DataFrame, x is optional + default_params = {"size": 1, "alpha": 0.5, "showlegend": False, "multicolor": False} def _get_color_palette(self, n_colors): diff --git a/ggplotly/geoms/geom_map.py b/ggplotly/geoms/geom_map.py index 2e8eb1f..7c915b7 100644 --- a/ggplotly/geoms/geom_map.py +++ b/ggplotly/geoms/geom_map.py @@ -53,6 +53,8 @@ def _extract_geojson(data): class geom_map(Geom): """Geom for drawing geographic maps (base maps, choropleths, and GeoJSON/sf).""" + required_aes = [] # Flexible - varies by mode (base map needs none, choropleth needs map_id) + def __init__(self, data=None, mapping=None, **params): """ Create a geographic map layer. diff --git a/ggplotly/geoms/geom_norm.py b/ggplotly/geoms/geom_norm.py index b1d86d6..d799167 100644 --- a/ggplotly/geoms/geom_norm.py +++ b/ggplotly/geoms/geom_norm.py @@ -54,6 +54,8 @@ class geom_norm(Geom): >>> ggplot(df, aes(x='x')) + geom_histogram(aes(y=after_stat('density'))) + geom_norm(color='blue', size=3) """ + required_aes = ['x'] + default_params = { "n": 101, "color": "red", diff --git a/ggplotly/geoms/geom_pacf.py b/ggplotly/geoms/geom_pacf.py index ee525d5..dc244fc 100644 --- a/ggplotly/geoms/geom_pacf.py +++ b/ggplotly/geoms/geom_pacf.py @@ -48,6 +48,8 @@ class geom_pacf(Geom): >>> ggplot(df, aes(y='value')) + geom_pacf(color='coral', nlags=30) """ + required_aes = ['y'] + default_params = { "nlags": 40, "alpha": 0.05, diff --git a/ggplotly/geoms/geom_path.py b/ggplotly/geoms/geom_path.py index 073f05d..637f11a 100644 --- a/ggplotly/geoms/geom_path.py +++ b/ggplotly/geoms/geom_path.py @@ -18,12 +18,13 @@ class geom_path(Geom): Parameters: color (str, optional): Color of the path. Can be a column name for grouping. + colour (str, optional): Alias for color (British spelling). size (float, optional): Line width. Default is 2. + linewidth (float, optional): Alias for size (ggplot2 3.4+ compatibility). linetype (str, optional): Line style ('solid', 'dash', 'dot', 'dashdot'). Default is 'solid'. alpha (float, optional): Transparency level. Default is 1. - lineend (str, optional): Line end style. Default is 'round'. - linejoin (str, optional): Line join style. Default is 'round'. - arrow (bool, optional): Whether to add arrow at end. Default is False. + na_rm (bool, optional): If True, remove missing values. Default is False. + show_legend (bool, optional): Whether to show in legend. Default is True. Aesthetics: - x: x-axis values @@ -45,8 +46,7 @@ class geom_path(Geom): ggplot(tracks, aes(x='x', y='y', color='track_id')) + geom_path() """ - __name__ = "geom_path" - + required_aes = ['x', 'y'] default_params = {"size": 2} def _draw_impl(self, fig, data, row, col): diff --git a/ggplotly/geoms/geom_point.py b/ggplotly/geoms/geom_point.py index b78df35..dea6a2e 100644 --- a/ggplotly/geoms/geom_point.py +++ b/ggplotly/geoms/geom_point.py @@ -22,10 +22,19 @@ class geom_point(Geom): shape (str, optional): Shape of the points. If a column name, maps categories to shapes. Literal values can be any Plotly marker symbol (e.g., 'circle', 'square', 'diamond', 'cross', 'x', 'triangle-up', 'triangle-down', 'star', 'hexagon', etc.) + stroke (float, optional): Width of the point border/outline. Default is 0. + In ggplot2, this applies to shapes 21-25 (filled shapes with borders). group (str, optional): Grouping variable for the points. + + Required Aesthetics: + x, y + + Optional Aesthetics: + color, fill, size, shape, alpha, group """ - default_params = {"size": 8} + required_aes = ['x', 'y'] + default_params = {"size": 8, "stroke": 0} def _draw_impl(self, fig, data, row, col): @@ -40,9 +49,15 @@ def _draw_impl(self, fig, data, row, col): self._draw_geo(fig, data) else: plot = go.Scatter + + # Handle stroke (border width) for markers + stroke = self.params.get("stroke", 0) + marker_line = {"width": stroke} if stroke else {} + payload = dict( mode="markers", name=self.params.get("name", "Point"), + marker_line=marker_line, ) color_targets = dict( @@ -114,11 +129,14 @@ def _draw_geo(self, fig, data): marker_size = size_val if isinstance(size_val, (int, float)) else 8 # Create marker dict + stroke = self.params.get('stroke', 0) marker_dict = dict( size=marker_size, opacity=alpha, symbol=self.params.get('shape', 'circle'), ) + if stroke: + marker_dict['line'] = {'width': stroke} if colorscale: marker_dict['color'] = marker_color diff --git a/ggplotly/geoms/geom_point_3d.py b/ggplotly/geoms/geom_point_3d.py index fc26c8d..3523b8f 100644 --- a/ggplotly/geoms/geom_point_3d.py +++ b/ggplotly/geoms/geom_point_3d.py @@ -71,6 +71,7 @@ class geom_point_3d(Geom): >>> ggplot(df, aes(x='x', y='y', z='z')) + geom_point_3d(size=10, alpha=0.7) """ + required_aes = ['x', 'y', 'z'] default_params = {"size": 6} def _convert_symbol_to_3d(self, symbol): diff --git a/ggplotly/geoms/geom_qq.py b/ggplotly/geoms/geom_qq.py index ea036fa..f457d4e 100644 --- a/ggplotly/geoms/geom_qq.py +++ b/ggplotly/geoms/geom_qq.py @@ -74,6 +74,7 @@ class geom_qq(Geom): ... + geom_qq()) """ + required_aes = ['sample'] default_params = {"size": 8} def __init__(self, data=None, mapping=None, distribution=None, dparams=None, **params): diff --git a/ggplotly/geoms/geom_qq_line.py b/ggplotly/geoms/geom_qq_line.py index 53894bd..7f4a521 100644 --- a/ggplotly/geoms/geom_qq_line.py +++ b/ggplotly/geoms/geom_qq_line.py @@ -71,6 +71,7 @@ class geom_qq_line(Geom): ... + geom_qq_line(distribution=stats.t, dparams={'df': 5})) """ + required_aes = ['sample'] default_params = {"size": 1.5, "color": "red", "linetype": "dashed"} def __init__(self, data=None, mapping=None, distribution=None, dparams=None, diff --git a/ggplotly/geoms/geom_range.py b/ggplotly/geoms/geom_range.py index 4a06d73..84c7529 100644 --- a/ggplotly/geoms/geom_range.py +++ b/ggplotly/geoms/geom_range.py @@ -61,7 +61,7 @@ class geom_range(Geom): ) """ - __name__ = "geom_range" + required_aes = ['x', 'y'] def __init__(self, data=None, mapping=None, **params): """ diff --git a/ggplotly/geoms/geom_rect.py b/ggplotly/geoms/geom_rect.py new file mode 100644 index 0000000..2eb671d --- /dev/null +++ b/ggplotly/geoms/geom_rect.py @@ -0,0 +1,178 @@ +# geoms/geom_rect.py + +import plotly.graph_objects as go + +from .geom_base import Geom + + +class geom_rect(Geom): + """ + Geom for drawing rectangles. + + Rectangles are defined by their corner coordinates (xmin, xmax, ymin, ymax). + Useful for highlighting regions, drawing backgrounds, or creating custom shapes. + + Parameters: + fill (str, optional): Fill color for the rectangles. Default is theme color. + color (str, optional): Border color for the rectangles. Default is None (no border). + alpha (float, optional): Transparency level (0-1). Default is 0.5. + linetype (str, optional): Border line style ('solid', 'dash', 'dot', 'dashdot'). + Default is 'solid'. + size (float, optional): Border line width. Default is 1. + linewidth (float, optional): Alias for size (ggplot2 3.4+ compatibility). + + Aesthetics: + - xmin: Left edge of rectangle (required) + - xmax: Right edge of rectangle (required) + - ymin: Bottom edge of rectangle (required) + - ymax: Top edge of rectangle (required) + - fill: Fill color (optional, can be mapped to variable) + - color: Border color (optional) + - group: Grouping variable (optional) + + Examples: + >>> # Highlight a region + >>> df = pd.DataFrame({'xmin': [2], 'xmax': [4], 'ymin': [1], 'ymax': [3]}) + >>> ggplot(df, aes(xmin='xmin', xmax='xmax', ymin='ymin', ymax='ymax')) + geom_rect() + + >>> # Multiple rectangles with fill mapping + >>> df = pd.DataFrame({ + ... 'xmin': [1, 3], 'xmax': [2, 4], + ... 'ymin': [1, 2], 'ymax': [3, 4], + ... 'category': ['A', 'B'] + ... }) + >>> ggplot(df, aes(xmin='xmin', xmax='xmax', ymin='ymin', ymax='ymax', fill='category')) + geom_rect() + + >>> # Rectangle with border + >>> ggplot(df, aes(xmin='xmin', xmax='xmax', ymin='ymin', ymax='ymax')) + geom_rect( + ... fill='lightblue', color='navy', size=2 + ... ) + + >>> # Highlight region on existing plot + >>> highlight = pd.DataFrame({'xmin': [2], 'xmax': [4], 'ymin': [0], 'ymax': [10]}) + >>> (ggplot(data, aes(x='x', y='y')) + ... + geom_rect(data=highlight, aes(xmin='xmin', xmax='xmax', ymin='ymin', ymax='ymax'), + ... fill='yellow', alpha=0.3) + ... + geom_point()) + """ + + required_aes = ['xmin', 'xmax', 'ymin', 'ymax'] + default_params = {"alpha": 0.5, "size": 1, "linetype": "solid"} + + def _draw_impl(self, fig, data, row, col): + style_props = self._get_style_props(data) + + xmin = data[self.mapping["xmin"]] + xmax = data[self.mapping["xmax"]] + ymin = data[self.mapping["ymin"]] + ymax = data[self.mapping["ymax"]] + + linetype = self.params.get("linetype", "solid") + linewidth = style_props.get("size", 1) + alpha = style_props["alpha"] + group_values = style_props["group_series"] + + # Border color - use 'color' param or None for no border + border_color = self.params.get("color", None) + + color_targets = dict(fill="fillcolor") + + def add_rect_trace(x_min, x_max, y_min, y_max, fill_color, name, showlegend=True, legendgroup=None): + """Helper to add a single rectangle as a filled scatter trace.""" + # Create rectangle path (clockwise from bottom-left) + x_path = [x_min, x_min, x_max, x_max, x_min] + y_path = [y_min, y_max, y_max, y_min, y_min] + + line_config = None + if border_color: + line_config = dict(color=border_color, width=linewidth, dash=linetype) + + fig.add_trace( + go.Scatter( + x=x_path, + y=y_path, + mode="lines", + fill="toself", + fillcolor=fill_color, + line=line_config if line_config else dict(width=0), + opacity=alpha, + name=name, + legendgroup=legendgroup or name, + showlegend=showlegend, + hoverinfo="name", + ), + row=row, + col=col, + ) + + # Handle grouped or colored rectangles + if group_values is not None: + # Case 1: Grouped by 'group' aesthetic + for group in group_values.unique(): + group_mask = group_values == group + if style_props["fill_series"] is not None: + trace_props = self._apply_color_targets(color_targets, style_props, value_key=group) + else: + trace_props = self._apply_color_targets(color_targets, style_props) + + fill_color = trace_props.get("fillcolor", style_props["default_color"]) + + for i, idx in enumerate(data[group_mask].index): + add_rect_trace( + xmin[idx], xmax[idx], ymin[idx], ymax[idx], + fill_color, str(group), + showlegend=(i == 0), + legendgroup=str(group) + ) + + elif style_props["fill_series"] is not None: + # Case 2: Fill mapped to categorical variable + fill_map = style_props["fill_map"] + fill_col = style_props["fill"] + + for cat_value in fill_map.keys(): + cat_mask = data[fill_col] == cat_value + trace_props = self._apply_color_targets(color_targets, style_props, value_key=cat_value) + fill_color = trace_props.get("fillcolor", style_props["default_color"]) + + for i, idx in enumerate(data[cat_mask].index): + add_rect_trace( + xmin[idx], xmax[idx], ymin[idx], ymax[idx], + fill_color, str(cat_value), + showlegend=(i == 0), + legendgroup=str(cat_value) + ) + + elif style_props["color_series"] is not None: + # Case 3: Color mapped to categorical variable (use for fill) + color_map = style_props["color_map"] + color_col = style_props["color"] + + for cat_value in color_map.keys(): + cat_mask = data[color_col] == cat_value + fill_color = color_map.get(cat_value, style_props["default_color"]) + + for i, idx in enumerate(data[cat_mask].index): + add_rect_trace( + xmin[idx], xmax[idx], ymin[idx], ymax[idx], + fill_color, str(cat_value), + showlegend=(i == 0), + legendgroup=str(cat_value) + ) + + else: + # Case 4: No grouping - single color for all rectangles + # Check for explicit fill param first (literal color value) + fill_color = self.params.get("fill") + if fill_color is None: + trace_props = self._apply_color_targets(color_targets, style_props) + fill_color = trace_props.get("fillcolor", style_props["default_color"]) + + name = self.params.get("name", "Rectangle") + for i, idx in enumerate(data.index): + add_rect_trace( + xmin[idx], xmax[idx], ymin[idx], ymax[idx], + fill_color, name, + showlegend=(i == 0), + legendgroup="rect" + ) diff --git a/ggplotly/geoms/geom_ribbon.py b/ggplotly/geoms/geom_ribbon.py index 9b5ab39..700724f 100644 --- a/ggplotly/geoms/geom_ribbon.py +++ b/ggplotly/geoms/geom_ribbon.py @@ -20,11 +20,22 @@ class geom_ribbon(Geom): ymin (float): Minimum y value for the ribbon. ymax (float): Maximum y value for the ribbon. fill (str, optional): Fill color for the ribbon. + color (str, optional): Color of the ribbon outline. alpha (float, optional): Transparency level for the fill color. Default is 0.5. + size (float, optional): Line width for the ribbon outline. Default is 1. + linewidth (float, optional): Alias for size (ggplot2 3.4+ compatibility). + linetype (str, optional): Line style for the outline ('solid', 'dash', etc.). group (str, optional): Grouping variable for the ribbon. + na_rm (bool, optional): If True, remove missing values. Default is False. + show_legend (bool, optional): Whether to show in legend. Default is True. + + Examples: + >>> ggplot(df, aes(x='x', ymin='lower', ymax='upper')) + geom_ribbon() + >>> ggplot(df, aes(x='x', ymin='lower', ymax='upper')) + geom_ribbon(fill='blue', alpha=0.3) """ - __name__ = "geom_ribbon" + required_aes = ['x', 'ymin', 'ymax'] + default_params = {"alpha": 0.5, "size": 1} def before_add(self): # Use color from params if specified, otherwise let geom_line use defaults diff --git a/ggplotly/geoms/geom_rug.py b/ggplotly/geoms/geom_rug.py index 4ff3891..2b6d4c0 100644 --- a/ggplotly/geoms/geom_rug.py +++ b/ggplotly/geoms/geom_rug.py @@ -9,6 +9,7 @@ class geom_rug(Geom): """Geom for drawing rug plots (marginal tick marks on axes).""" + required_aes = [] # x and/or y - flexible, at least one recommended default_params = {"size": 1, "alpha": 0.5} def __init__(self, data=None, mapping=None, **params): diff --git a/ggplotly/geoms/geom_sankey.py b/ggplotly/geoms/geom_sankey.py index 2618d47..17703f0 100644 --- a/ggplotly/geoms/geom_sankey.py +++ b/ggplotly/geoms/geom_sankey.py @@ -88,6 +88,8 @@ class geom_sankey(Geom): Each row represents a flow from source to target with the given value. """ + required_aes = ['source', 'target', 'value'] + def __init__(self, data=None, mapping=None, **params): super().__init__(data, mapping, **params) diff --git a/ggplotly/geoms/geom_searoute.py b/ggplotly/geoms/geom_searoute.py index 631d61f..3e0ef46 100644 --- a/ggplotly/geoms/geom_searoute.py +++ b/ggplotly/geoms/geom_searoute.py @@ -25,6 +25,8 @@ def _get_searoute(): class geom_searoute(Geom): """Sea routes for maritime visualization using the searoute package.""" + required_aes = ['x', 'y', 'xend', 'yend'] + def __init__( self, mapping=None, diff --git a/ggplotly/geoms/geom_segment.py b/ggplotly/geoms/geom_segment.py index f2487c9..2f47972 100644 --- a/ggplotly/geoms/geom_segment.py +++ b/ggplotly/geoms/geom_segment.py @@ -16,12 +16,25 @@ class geom_segment(Geom): xend (float): End x-coordinate of the line segment. yend (float): End y-coordinate of the line segment. color (str, optional): Color of the segment lines. + colour (str, optional): Alias for color (British spelling). + size (float, optional): Line width. Default is 2. + linewidth (float, optional): Alias for size (ggplot2 3.4+ compatibility). linetype (str, optional): Line style ('solid', 'dash', etc.). Default is 'solid'. alpha (float, optional): Transparency level for the segments. Default is 1. group (str, optional): Grouping variable for the segments. + arrow (bool, optional): If True, adds an arrowhead at the end of the segment. + Default is False. + arrow_size (int, optional): Size of the arrowhead. Default is 15. + na_rm (bool, optional): If True, remove missing values. Default is False. + show_legend (bool, optional): Whether to show in legend. Default is True. + + Examples: + >>> ggplot(df, aes(x='x', y='y', xend='xend', yend='yend')) + geom_segment() + >>> ggplot(df, aes(x='x', y='y', xend='xend', yend='yend')) + geom_segment(arrow=True) """ - default_params = {"size": 2} + required_aes = ['x', 'y', 'xend', 'yend'] + default_params = {"size": 2, "arrow": False, "arrow_size": 15} def _draw_impl(self, fig, data, row, col): style_props = self._get_style_props(data) @@ -35,6 +48,20 @@ def _draw_impl(self, fig, data, row, col): alpha = style_props['alpha'] group_values = style_props['group_series'] + # Arrow configuration + arrow = self.params.get("arrow", False) + arrow_size = self.params.get("arrow_size", 15) + if arrow: + mode = "lines+markers" + marker_config = dict( + symbol=["circle", "arrow"], + size=[0, arrow_size], + angleref="previous", + ) + else: + mode = "lines" + marker_config = None + color_targets = dict(color="line_color") # Handle grouped or colored segments @@ -50,24 +77,22 @@ def _draw_impl(self, fig, data, row, col): # Create separate segments for each data point in the group for i, idx in enumerate(data[group_mask].index): - fig.add_trace( - go.Scatter( - x=[x[idx], xend[idx]], - y=[y[idx], yend[idx]], - mode="lines", - line_dash=linetype, - opacity=alpha, - name=str(group), - legendgroup=str(group), - showlegend=(i == 0), # Only show legend for first segment - **trace_props, - ), - row=row, - col=col, + scatter_kwargs = dict( + x=[x[idx], xend[idx]], + y=[y[idx], yend[idx]], + mode=mode, + line_dash=linetype, + opacity=alpha, + name=str(group), + legendgroup=str(group), + showlegend=(i == 0), # Only show legend for first segment + **trace_props, ) + if marker_config: + scatter_kwargs["marker"] = marker_config + fig.add_trace(go.Scatter(**scatter_kwargs), row=row, col=col) elif style_props['color_series'] is not None: # Case 2: Colored by categorical variable - style_props['color_series'] cat_map = style_props['color_map'] cat_col = style_props['color'] @@ -77,38 +102,36 @@ def _draw_impl(self, fig, data, row, col): # Create separate segments for each data point in the category for i, idx in enumerate(data[cat_mask].index): - fig.add_trace( - go.Scatter( - x=[x[idx], xend[idx]], - y=[y[idx], yend[idx]], - mode="lines", - line_dash=linetype, - opacity=alpha, - name=str(cat_value), - legendgroup=str(cat_value), - showlegend=(i == 0), # Only show legend for first segment - **trace_props, - ), - row=row, - col=col, - ) - else: - # Case 3: No grouping or categorical coloring - single trace per segment - trace_props = self._apply_color_targets(color_targets, style_props) - - for i, idx in enumerate(data.index): - fig.add_trace( - go.Scatter( + scatter_kwargs = dict( x=[x[idx], xend[idx]], y=[y[idx], yend[idx]], - mode="lines", + mode=mode, line_dash=linetype, opacity=alpha, - name=self.params.get("name", "Segment"), - legendgroup="segment", + name=str(cat_value), + legendgroup=str(cat_value), showlegend=(i == 0), # Only show legend for first segment **trace_props, - ), - row=row, - col=col, + ) + if marker_config: + scatter_kwargs["marker"] = marker_config + fig.add_trace(go.Scatter(**scatter_kwargs), row=row, col=col) + else: + # Case 3: No grouping or categorical coloring - single trace per segment + trace_props = self._apply_color_targets(color_targets, style_props) + + for i, idx in enumerate(data.index): + scatter_kwargs = dict( + x=[x[idx], xend[idx]], + y=[y[idx], yend[idx]], + mode=mode, + line_dash=linetype, + opacity=alpha, + name=self.params.get("name", "Segment"), + legendgroup="segment", + showlegend=(i == 0), # Only show legend for first segment + **trace_props, ) + if marker_config: + scatter_kwargs["marker"] = marker_config + fig.add_trace(go.Scatter(**scatter_kwargs), row=row, col=col) diff --git a/ggplotly/geoms/geom_smooth.py b/ggplotly/geoms/geom_smooth.py index 5796e1c..ed1501d 100644 --- a/ggplotly/geoms/geom_smooth.py +++ b/ggplotly/geoms/geom_smooth.py @@ -23,17 +23,24 @@ class geom_smooth(Geom): Larger values (closer to 1) produce smoother lines. Default is 2/3 (~0.667) to match R. se (bool, optional): Whether to display confidence interval ribbon. Default is True to match R. level (float, optional): Confidence level for the interval (e.g., 0.95 for 95% CI). Default is 0.95 to match R. + fullrange (bool, optional): If True, extend the smooth line to fill the full x-axis range. + Default is False (smooth only within data range). + n (int, optional): Number of points to evaluate predictions at. Default is 80. color (str, optional): Color of the smooth lines. If a categorical variable is mapped to color, different colors will be assigned. linetype (str, optional): Line type ('solid', 'dash', etc.). Default is 'solid'. alpha (float, optional): Transparency level for the smooth lines. Default is 1. group (str, optional): Grouping variable for the smooth lines. + na_rm (bool, optional): If True, remove missing values. Default is False. + show_legend (bool, optional): Whether to show in legend. Default is True. Examples: >>> ggplot(df, aes(x='x', y='y')) + geom_point() + geom_smooth() >>> ggplot(df, aes(x='x', y='y')) + geom_point() + geom_smooth(method='lm', se=False) + >>> ggplot(df, aes(x='x', y='y')) + geom_point() + geom_smooth(fullrange=True) """ - default_params = {"size": 3} + required_aes = ['x', 'y'] + default_params = {"size": 3, "fullrange": False, "n": 80} def _draw_impl(self, fig, data, row, col): """ @@ -62,10 +69,24 @@ def _draw_impl(self, fig, data, row, col): se = self.params.get("se", True) # Default to True to match R level = self.params.get("level", 0.95) # Default to 95% CI to match R span = self.params.get("span", 2/3) # Default to 2/3 to match R's loess + fullrange = self.params.get("fullrange", False) # Extend to full x range + n_points = self.params.get("n", 80) # Number of prediction points # Initialize stat_smooth for statistical smoothing smoother = stat_smooth(method=method, span=span, se=se, level=level) + # Handle fullrange: extend x values beyond data range + x_col = self.mapping.get("x", "x") + if fullrange and x_col in data.columns: + import numpy as np + x_min, x_max = data[x_col].min(), data[x_col].max() + x_range = x_max - x_min + # Extend by 5% on each side + extended_min = x_min - 0.05 * x_range + extended_max = x_max + 0.05 * x_range + # Store for later use + self._fullrange_x = np.linspace(extended_min, extended_max, n_points) + # Get the actual column names from the mapping x_col = self.mapping.get("x", "x") y_col = self.mapping.get("y", "y") diff --git a/ggplotly/geoms/geom_step.py b/ggplotly/geoms/geom_step.py index cbc29d9..db59f47 100644 --- a/ggplotly/geoms/geom_step.py +++ b/ggplotly/geoms/geom_step.py @@ -15,14 +15,33 @@ class geom_step(Geom): Parameters: color (str, optional): Color of the steps. + colour (str, optional): Alias for color (British spelling). + size (float, optional): Line width. Default is 2. + linewidth (float, optional): Alias for size (ggplot2 3.4+ compatibility). linetype (str, optional): Line style ('solid', 'dash', etc.). Default is 'solid'. alpha (float, optional): Transparency level for the steps. Default is 1. group (str, optional): Grouping variable for the steps. stat (str, optional): The statistical transformation to use. Default is 'identity'. + Use 'ecdf' for empirical cumulative distribution function. + na_rm (bool, optional): If True, remove missing values. Default is False. + show_legend (bool, optional): Whether to show in legend. Default is True. + + Examples: + >>> ggplot(df, aes(x='x', y='y')) + geom_step() + >>> ggplot(df, aes(x='x')) + geom_step(stat='ecdf') """ + required_aes = ['x'] # y is optional when stat='ecdf' default_params = {"size": 2} + def _apply_stats(self, data): + """Add stat_ecdf if stat='ecdf'.""" + if self.stats == []: + stat = self.params.get("stat", "identity") + if stat == "ecdf": + self.stats.append(stat_ecdf(mapping=self.mapping)) + return super()._apply_stats(data) + def _draw_impl(self, fig, data, row, col): # Remove size from mapping if present - step lines can't have variable widths @@ -30,12 +49,6 @@ def _draw_impl(self, fig, data, row, col): if "size" in self.mapping: del self.mapping["size"] - # Handle ECDF transformation using stat_ecdf - stat = self.params.get("stat", "identity") - if stat == "ecdf": - ecdf_stat = stat_ecdf(mapping=self.mapping) - data, self.mapping = ecdf_stat.compute(data) - plot = go.Scatter line_dash = self.params.get("linetype", "solid") diff --git a/ggplotly/geoms/geom_stl.py b/ggplotly/geoms/geom_stl.py index 778e5bc..08fb321 100644 --- a/ggplotly/geoms/geom_stl.py +++ b/ggplotly/geoms/geom_stl.py @@ -50,6 +50,8 @@ class geom_stl(Geom): >>> ggplot(df, aes(x='date', y='value')) + geom_stl(period=12, color='coral') """ + required_aes = ['x', 'y'] + default_params = { "color": "steelblue", "line_width": 1.5, diff --git a/ggplotly/geoms/geom_surface.py b/ggplotly/geoms/geom_surface.py index c24be00..52fc494 100644 --- a/ggplotly/geoms/geom_surface.py +++ b/ggplotly/geoms/geom_surface.py @@ -9,6 +9,8 @@ class geom_surface(Geom): """Geom for drawing 3D surface plots.""" + required_aes = ['x', 'y', 'z'] + def __init__(self, data=None, mapping=None, **params): """ Create a 3D surface plot. @@ -230,6 +232,8 @@ class geom_wireframe(Geom): >>> ggplot(df, aes(x='x', y='y', z='z')) + geom_wireframe(color='red', linewidth=2) """ + required_aes = ['x', 'y', 'z'] + def _prepare_grid_data(self, data): """Convert long-format data to grid format. diff --git a/ggplotly/geoms/geom_text.py b/ggplotly/geoms/geom_text.py index b4a6beb..6f89217 100644 --- a/ggplotly/geoms/geom_text.py +++ b/ggplotly/geoms/geom_text.py @@ -2,6 +2,7 @@ import plotly.graph_objects as go +from ..aesthetic_mapper import AestheticMapper from .geom_base import Geom @@ -26,6 +27,8 @@ class geom_text(Geom): family (str, optional): Font family. Default is None (use Plotly default). fontface (str, optional): Font face ('plain', 'bold', 'italic', 'bold.italic'). Default is 'plain'. + parse (bool, optional): If True, parse text labels as LaTeX math expressions. + Labels will be rendered using MathJax. Default is False. na_rm (bool, optional): If True, silently remove missing values. Default is False. textposition (str, optional): Direct Plotly textposition override ('top center', 'middle right', etc.). If provided, overrides hjust/vjust. @@ -40,6 +43,8 @@ class geom_text(Geom): >>> ggplot(df, aes(x='x', y='y', label='name')) + geom_text(nudge_y=0.5) """ + required_aes = ['x', 'y', 'label'] + def _hjust_vjust_to_textposition(self, hjust, vjust): """ Convert hjust/vjust values to Plotly textposition string. @@ -101,7 +106,6 @@ def _draw_impl(self, fig, data, row, col): data = data.dropna(subset=cols_to_check) # Create aesthetic mapper for this geom - from ..aesthetic_mapper import AestheticMapper mapper = AestheticMapper(data, self.mapping, self.params, self.theme) style_props = mapper.get_style_properties() @@ -109,6 +113,11 @@ def _draw_impl(self, fig, data, row, col): y = data[self.mapping["y"]].copy() label = data[self.mapping["label"]] # Use 'label' mapping instead of 'text' + # Handle parse parameter - wrap labels in $...$ for MathJax rendering + parse = self.params.get("parse", False) + if parse: + label = label.apply(lambda t: f"${t}$" if not str(t).startswith("$") else t) + # Apply nudge offsets nudge_x = self.params.get("nudge_x", 0) nudge_y = self.params.get("nudge_y", 0) @@ -125,14 +134,17 @@ def _draw_impl(self, fig, data, row, col): vjust = self.params.get("vjust", 0.5) textposition = self._hjust_vjust_to_textposition(hjust, vjust) - # Get angle parameter (note: Plotly uses clockwise, R uses counter-clockwise) - self.params.get("angle", 0) - # Get text styling parameters - self.params.get("size", 11) - self.params.get("family", None) + text_size = self.params.get("size", 11) + text_family = self.params.get("family", None) fontface = self.params.get("fontface", "plain") - self._fontface_to_plotly(fontface) + font_style = self._fontface_to_plotly(fontface) + + # Build textfont configuration + textfont = {"size": text_size} + if text_family: + textfont["family"] = text_family + textfont.update(font_style) alpha = style_props['alpha'] group_values = style_props['group_series'] @@ -156,6 +168,7 @@ def _draw_impl(self, fig, data, row, col): mode="text", text=label[group_mask], textposition=textposition, + textfont=textfont, opacity=alpha, showlegend=False, name=str(group), @@ -166,7 +179,6 @@ def _draw_impl(self, fig, data, row, col): ) elif style_props['color_series'] is not None: # Case 2: Colored by categorical variable - style_props['color_series'] cat_map = style_props['color_map'] cat_col = style_props['color'] @@ -181,6 +193,7 @@ def _draw_impl(self, fig, data, row, col): mode="text", text=label[cat_mask], textposition=textposition, + textfont=textfont, opacity=alpha, showlegend=False, name=str(cat_value), @@ -199,6 +212,7 @@ def _draw_impl(self, fig, data, row, col): mode="text", text=label, textposition=textposition, + textfont=textfont, opacity=alpha, showlegend=False, name=self.params.get("name", "Text"), diff --git a/ggplotly/geoms/geom_tile.py b/ggplotly/geoms/geom_tile.py index 046901d..5fc2ccf 100644 --- a/ggplotly/geoms/geom_tile.py +++ b/ggplotly/geoms/geom_tile.py @@ -21,11 +21,12 @@ class geom_tile(Geom): group (str, optional): Grouping variable for the tiles. """ + required_aes = ['x', 'y'] + def _draw_impl(self, fig, data, row, col): x = data[self.mapping["x"]] y = data[self.mapping["y"]] z = data[self.mapping["fill"]] if "fill" in self.mapping else None - data[self.mapping["group"]] if "group" in self.mapping else None alpha = self.params.get("alpha", 1) # Handle fill mapping if fill is categorical or continuous diff --git a/ggplotly/geoms/geom_violin.py b/ggplotly/geoms/geom_violin.py index 1c513d5..0d8f696 100644 --- a/ggplotly/geoms/geom_violin.py +++ b/ggplotly/geoms/geom_violin.py @@ -20,6 +20,8 @@ class geom_violin(Geom): group (str, optional): Grouping variable for the violin plots. """ + required_aes = ['x', 'y'] + def _draw_impl(self, fig, data, row, col): if "linewidth" not in self.params: self.params["linewidth"] = 1 diff --git a/ggplotly/geoms/geom_vline.py b/ggplotly/geoms/geom_vline.py index d0e03da..003f621 100644 --- a/ggplotly/geoms/geom_vline.py +++ b/ggplotly/geoms/geom_vline.py @@ -5,8 +5,7 @@ class geom_vline(Geom): """Geom for drawing vertical reference lines.""" - __name__ = "geom_vline" - + required_aes = [] # xintercept comes from data/params, not mapping default_params = {"size": 2} def __init__(self, data=None, mapping=None, **params): @@ -55,17 +54,7 @@ def _draw_impl(self, fig, data, row, col): if not isinstance(x, list): x = [x] - # Get color from params, or use theme default - color = self.params.get("color", None) - if color is None and hasattr(self, 'theme') and self.theme: - # Use first color from theme palette - import plotly.express as px - palette = self.theme.color_map if hasattr(self.theme, 'color_map') and self.theme.color_map else px.colors.qualitative.Plotly - color = palette[0] - elif color is None: - # Default to theme's default color - color = '#1f77b4' - + color = self._get_reference_line_color() linetype = self.params.get("linetype", "solid") alpha = self.params.get("alpha", 1) name = self.params.get("name", "vline") diff --git a/ggplotly/geoms/geom_waterfall.py b/ggplotly/geoms/geom_waterfall.py index ae19a6a..cc1205c 100644 --- a/ggplotly/geoms/geom_waterfall.py +++ b/ggplotly/geoms/geom_waterfall.py @@ -92,6 +92,8 @@ class geom_waterfall(Geom): mark starting values ('absolute') and subtotals ('total'). """ + required_aes = ['x', 'y'] + def __init__(self, data=None, mapping=None, **params): super().__init__(data, mapping, **params) # Set default colors diff --git a/ggplotly/ggplot.py b/ggplotly/ggplot.py index 6dacadc..58f5586 100644 --- a/ggplotly/ggplot.py +++ b/ggplotly/ggplot.py @@ -76,8 +76,6 @@ def __init__(self, data=None, mapping=None): self.guides_obj = None # Initialize guides self.fig = go.Figure() self.auto_draw = True # Automatically draw after adding components by default - self.color_map = None - self.is_geo = False # Track if plot uses geographic coordinates def copy(self): """ @@ -124,10 +122,20 @@ def __add__(self, other): self.add_component(other) return self.copy() + def _needs_mathjax(self): + """Check if any geom uses parse=True for LaTeX rendering.""" + for geom in self.layers: + if geom.params.get('parse', False): + return True + return False + def _repr_html_(self): """Return HTML representation for Jupyter/IPython display.""" if self.auto_draw: fig = self.draw() + # Only include MathJax CDN if parse=True is used (for LaTeX rendering) + if self._needs_mathjax(): + return fig.to_html(full_html=False, include_plotlyjs='cdn', include_mathjax='cdn') return fig._repr_html_() return "" @@ -135,8 +143,11 @@ def _repr_mimebundle_(self, **kwargs): """Return MIME bundle for Jupyter display (preferred by VS Code, JupyterLab).""" if self.auto_draw: fig = self.draw() + # Only include MathJax CDN if parse=True is used (for LaTeX rendering) + if self._needs_mathjax(): + html = fig.to_html(full_html=False, include_plotlyjs='cdn', include_mathjax='cdn') + return {'text/html': html} bundle = fig._repr_mimebundle_(**kwargs) - # Also include text/html for nbconvert/mkdocs-jupyter compatibility if 'text/html' not in bundle: bundle['text/html'] = fig._repr_html_() return bundle @@ -357,12 +368,17 @@ def save(self, filepath): Parameters: filepath (str): The file path where the plot should be saved. + + Note: + HTML files use CDN references for Plotly.js and MathJax, resulting + in small file sizes (~8KB) but requiring internet for viewing. + MathJax CDN enables LaTeX rendering (e.g., geom_text with parse=True). """ if not hasattr(self, "fig"): raise AttributeError("No figure to save. Call draw() before saving.") if filepath.endswith(".html"): - self.fig.write_html(filepath) + self.fig.write_html(filepath, include_plotlyjs='cdn', include_mathjax='cdn') elif filepath.endswith(".png"): self.fig.write_image(filepath) else: diff --git a/ggplotly/scales/__init__.py b/ggplotly/scales/__init__.py index dbc6700..9315320 100644 --- a/ggplotly/scales/__init__.py +++ b/ggplotly/scales/__init__.py @@ -14,8 +14,10 @@ from .scale_x_log10 import scale_x_log10 from .scale_x_rangeselector import scale_x_rangeselector from .scale_x_rangeslider import scale_x_rangeslider +from .scale_x_reverse import scale_x_reverse from .scale_y_continuous import scale_y_continuous from .scale_y_log10 import scale_y_log10 +from .scale_y_reverse import scale_y_reverse __all__ = [ "Scale", @@ -23,6 +25,8 @@ "scale_y_continuous", "scale_x_log10", "scale_y_log10", + "scale_x_reverse", + "scale_y_reverse", "scale_x_date", "scale_x_datetime", "scale_x_rangeslider", diff --git a/ggplotly/scales/scale_base.py b/ggplotly/scales/scale_base.py index 83e9103..3e4f115 100644 --- a/ggplotly/scales/scale_base.py +++ b/ggplotly/scales/scale_base.py @@ -44,6 +44,59 @@ def apply(self, fig): """ pass # To be implemented by subclasses + def _apply_manual_color_mapping(self, fig, values, name=None, breaks=None, + labels=None, update_fill=False, guide='legend'): + """ + Apply manual color mapping to figure traces. + + Shared implementation for scale_color_manual and scale_fill_manual. + + Parameters: + fig (Figure): Plotly figure object. + values (dict or list): Color mapping or list of colors. + name (str, optional): Legend title. + breaks (list, optional): Categories to show in legend. + labels (list, optional): Labels for breaks. + update_fill (bool): If True, also update fillcolor attribute. + guide (str): 'legend' or 'none' to control legend visibility. + """ + # Create a mapping of categories to colors + if isinstance(values, dict): + color_map = values + else: + # Extract categories from trace names + categories = [] + for trace in fig.data: + if hasattr(trace, 'name') and trace.name not in categories: + categories.append(trace.name) + color_map = dict(zip(categories, values)) + + # Update trace colors based on the mapping + for trace in fig.data: + if hasattr(trace, 'name') and trace.name in color_map: + color = color_map[trace.name] + if hasattr(trace, 'marker') and trace.marker is not None: + trace.marker.color = color + if hasattr(trace, 'line') and trace.line is not None: + trace.line.color = color + if update_fill and hasattr(trace, 'fillcolor'): + trace.fillcolor = color + + # Update the legend title if provided + if name is not None: + fig.update_layout(legend_title_text=name) + + # Update legend items if breaks and labels are provided + if breaks is not None and labels is not None: + for trace in fig.data: + if hasattr(trace, 'name') and trace.name in breaks: + idx = breaks.index(trace.name) + trace.name = labels[idx] + + # Hide legend if guide is 'none' + if guide == 'none': + fig.update_layout(showlegend=False) + class ScaleRegistry: """ diff --git a/ggplotly/scales/scale_color_gradient.py b/ggplotly/scales/scale_color_gradient.py index 22bef90..5b39566 100644 --- a/ggplotly/scales/scale_color_gradient.py +++ b/ggplotly/scales/scale_color_gradient.py @@ -61,14 +61,17 @@ def __init__(self, low="#132B43", high="#56B1F7", name=None, limits=None, def apply(self, fig): """ - Apply the color gradient to markers in the figure. + Apply the color gradient to markers and line segments in the figure. Parameters: fig (Figure): Plotly figure object. """ + new_colorscale = [[0, self.low], [1, self.high]] + for trace in fig.data: + # Handle marker-based traces (scatter points, etc.) if hasattr(trace, 'marker') and trace.marker is not None: - trace.marker.colorscale = [[0, self.low], [1, self.high]] + trace.marker.colorscale = new_colorscale # Apply limits if specified if self.limits is not None: @@ -89,3 +92,58 @@ def apply(self, fig): trace.marker.showscale = True else: trace.marker.showscale = False + + # Handle line gradient segments (created by ContinuousColorTraceBuilder) + if hasattr(trace, 'meta') and trace.meta: + meta = trace.meta + if isinstance(meta, dict) and meta.get('_ggplotly_line_gradient'): + t_norm = meta.get('_color_norm', 0) + new_color = self._interpolate_color(new_colorscale, t_norm) + trace.line.color = new_color + + @staticmethod + def _interpolate_color(colorscale, t): + """ + Interpolate between colorscale endpoints. + + Parameters: + colorscale: List of [position, color] pairs + t: Normalized value between 0 and 1 + + Returns: + str: Interpolated RGB color string + """ + t = max(0, min(1, t)) # Clamp to [0, 1] + + low_color = colorscale[0][1] + high_color = colorscale[1][1] + + # Parse color to RGB (handles hex and named colors) + def color_to_rgb(color): + if color.startswith('#'): + color = color.lstrip('#') + return tuple(int(color[i:i + 2], 16) for i in (0, 2, 4)) + elif color.startswith('rgb'): + # Parse rgb(r, g, b) format + import re + match = re.match(r'rgb\((\d+),\s*(\d+),\s*(\d+)\)', color) + if match: + return tuple(int(x) for x in match.groups()) + # Fallback for named colors - approximate mapping + named_colors = { + 'blue': (0, 0, 255), 'red': (255, 0, 0), 'green': (0, 128, 0), + 'white': (255, 255, 255), 'black': (0, 0, 0), + 'yellow': (255, 255, 0), 'orange': (255, 165, 0), + 'purple': (128, 0, 128), 'cyan': (0, 255, 255), + } + return named_colors.get(color.lower(), (128, 128, 128)) + + low_rgb = color_to_rgb(low_color) + high_rgb = color_to_rgb(high_color) + + # Linear interpolation + r = int(low_rgb[0] + t * (high_rgb[0] - low_rgb[0])) + g = int(low_rgb[1] + t * (high_rgb[1] - low_rgb[1])) + b = int(low_rgb[2] + t * (high_rgb[2] - low_rgb[2])) + + return f'rgb({r}, {g}, {b})' diff --git a/ggplotly/scales/scale_color_manual.py b/ggplotly/scales/scale_color_manual.py index fdec437..6341ea5 100644 --- a/ggplotly/scales/scale_color_manual.py +++ b/ggplotly/scales/scale_color_manual.py @@ -28,35 +28,8 @@ def apply(self, fig): Parameters: fig (Figure): Plotly figure object. """ - # Create a mapping of categories to colors - if isinstance(self.values, dict): - color_map = self.values - else: - # Assume values is a list; extract categories from the data - categories = [] - for trace in fig.data: - if "name" in trace and trace.name not in categories: - categories.append(trace.name) - color_map = dict(zip(categories, self.values)) - - # Update trace colors based on the mapping - for trace in fig.data: - if "name" in trace and trace.name in color_map: - color = color_map[trace.name] - if "marker" in trace: - trace.marker.color = color - elif "line" in trace: - trace.line.color = color - elif "fillcolor" in trace: - trace.fillcolor = color - - # Update the legend title if provided - if self.name is not None: - fig.update_layout(legend_title_text=self.name) - - # Update legend items if breaks and labels are provided - if self.breaks is not None and self.labels is not None: - for trace in fig.data: - if trace.name in self.breaks: - idx = self.breaks.index(trace.name) - trace.name = self.labels[idx] + self._apply_manual_color_mapping( + fig, self.values, name=self.name, + breaks=self.breaks, labels=self.labels, + update_fill=False + ) diff --git a/ggplotly/scales/scale_fill_manual.py b/ggplotly/scales/scale_fill_manual.py index 542c77a..f1e9db4 100644 --- a/ggplotly/scales/scale_fill_manual.py +++ b/ggplotly/scales/scale_fill_manual.py @@ -83,40 +83,11 @@ def apply(self, fig): Parameters: fig (Figure): Plotly figure object. """ - # Create a mapping of categories to colors - if isinstance(self.values, dict): - color_map = self.values - else: - # Assume values is a list; extract categories from the data - categories = [] - for trace in fig.data: - if hasattr(trace, 'name') and trace.name not in categories: - categories.append(trace.name) - color_map = dict(zip(categories, self.values)) - - # Update trace colors based on the mapping - for trace in fig.data: - if hasattr(trace, 'name') and trace.name in color_map: - color = color_map[trace.name] - if hasattr(trace, 'marker') and trace.marker is not None: - trace.marker.color = color - if hasattr(trace, 'fillcolor'): - trace.fillcolor = color - - # Update the legend title if provided - if self.name is not None: - fig.update_layout(legend_title_text=self.name) - - # Update legend items if breaks and labels are provided - if self.breaks is not None and self.labels is not None: - for trace in fig.data: - if hasattr(trace, 'name') and trace.name in self.breaks: - idx = self.breaks.index(trace.name) - trace.name = self.labels[idx] - - # Hide legend if guide is 'none' - if self.guide == 'none': - fig.update_layout(showlegend=False) + self._apply_manual_color_mapping( + fig, self.values, name=self.name, + breaks=self.breaks, labels=self.labels, + update_fill=True, guide=self.guide + ) def get_legend_info(self): """ diff --git a/ggplotly/scales/scale_x_reverse.py b/ggplotly/scales/scale_x_reverse.py new file mode 100644 index 0000000..318e13c --- /dev/null +++ b/ggplotly/scales/scale_x_reverse.py @@ -0,0 +1,105 @@ +# scales/scale_x_reverse.py +"""Reversed scale for the x-axis.""" + +from .scale_base import Scale + + +class scale_x_reverse(Scale): + """ + Reverse the x-axis direction. + + This scale reverses the x-axis so that larger values appear on the left + and smaller values on the right. Useful for certain data presentations + like depth charts, time running backwards, etc. + + Aesthetic: x + + Parameters: + name (str, optional): Title for the x-axis. + breaks (list, optional): List of positions at which to place tick marks. + labels (list, optional): List of labels corresponding to the breaks. + Can also be a callable that takes breaks and returns labels. + limits (tuple, optional): Two-element tuple (min, max) for axis limits. + Note: min should still be less than max; the reversal happens automatically. + expand (tuple, optional): Expansion to add around the data range. + Default is (0.05, 0). + + Examples: + >>> # Simple reversed x-axis + >>> ggplot(df, aes(x='x', y='y')) + geom_point() + scale_x_reverse() + + >>> # Reversed x-axis with custom name + >>> ggplot(df, aes(x='depth', y='value')) + geom_line() + scale_x_reverse(name='Depth (m)') + + >>> # Reversed with specific limits + >>> ggplot(df, aes(x='x', y='y')) + geom_point() + scale_x_reverse(limits=(0, 100)) + + See Also: + scale_y_reverse: Reverse the y-axis + scale_x_continuous: Continuous x-axis with trans='reverse' option + """ + + aesthetic = 'x' + + def __init__(self, name=None, breaks=None, labels=None, limits=None, + expand=(0.05, 0)): + """ + Initialize the reversed x-axis scale. + + Parameters: + name (str, optional): Axis title. + breaks (list, optional): Tick positions. + labels (list or callable, optional): Labels for breaks. + limits (tuple, optional): Axis limits (min, max). + expand (tuple): Expansion factor (mult, add). Default is (0.05, 0). + """ + self.name = name + self.breaks = breaks + self.labels = labels + self.limits = limits + self.expand = expand + + def _apply_expansion(self, limits): + """Apply expansion to limits.""" + if limits is None or self.expand is None: + return limits + + low, high = limits + data_range = high - low + + mult = self.expand[0] + add = self.expand[1] if len(self.expand) > 1 else 0 + + new_low = low - data_range * mult - add + new_high = high + data_range * mult + add + return [new_low, new_high] + + def apply(self, fig): + """ + Apply reversed transformation to the x-axis. + + Parameters: + fig (Figure): Plotly figure object. + """ + xaxis_update = {"autorange": "reversed"} + + if self.name is not None: + xaxis_update["title_text"] = self.name + + if self.limits is not None: + expanded_limits = self._apply_expansion(self.limits) + # For reversed axis, we need to swap the order + xaxis_update["range"] = [expanded_limits[1], expanded_limits[0]] + # Remove autorange since we're setting explicit range + xaxis_update.pop("autorange") + + if self.breaks is not None: + xaxis_update["tickmode"] = "array" + xaxis_update["tickvals"] = self.breaks + if self.labels is not None: + if callable(self.labels): + xaxis_update["ticktext"] = self.labels(self.breaks) + else: + xaxis_update["ticktext"] = self.labels + + fig.update_xaxes(**xaxis_update) diff --git a/ggplotly/scales/scale_y_reverse.py b/ggplotly/scales/scale_y_reverse.py new file mode 100644 index 0000000..c292cd5 --- /dev/null +++ b/ggplotly/scales/scale_y_reverse.py @@ -0,0 +1,108 @@ +# scales/scale_y_reverse.py +"""Reversed scale for the y-axis.""" + +from .scale_base import Scale + + +class scale_y_reverse(Scale): + """ + Reverse the y-axis direction. + + This scale reverses the y-axis so that larger values appear at the bottom + and smaller values at the top. Useful for certain data presentations + like depth charts, rankings, or inverted coordinate systems. + + Aesthetic: y + + Parameters: + name (str, optional): Title for the y-axis. + breaks (list, optional): List of positions at which to place tick marks. + labels (list, optional): List of labels corresponding to the breaks. + Can also be a callable that takes breaks and returns labels. + limits (tuple, optional): Two-element tuple (min, max) for axis limits. + Note: min should still be less than max; the reversal happens automatically. + expand (tuple, optional): Expansion to add around the data range. + Default is (0.05, 0). + + Examples: + >>> # Simple reversed y-axis + >>> ggplot(df, aes(x='x', y='y')) + geom_point() + scale_y_reverse() + + >>> # Reversed y-axis with custom name (e.g., for rankings) + >>> ggplot(df, aes(x='name', y='rank')) + geom_col() + scale_y_reverse(name='Rank') + + >>> # Reversed with specific limits + >>> ggplot(df, aes(x='x', y='y')) + geom_point() + scale_y_reverse(limits=(0, 100)) + + >>> # Ocean depth chart (depth increases downward) + >>> ggplot(df, aes(x='distance', y='depth')) + geom_line() + scale_y_reverse() + + See Also: + scale_x_reverse: Reverse the x-axis + scale_y_continuous: Continuous y-axis with trans='reverse' option + """ + + aesthetic = 'y' + + def __init__(self, name=None, breaks=None, labels=None, limits=None, + expand=(0.05, 0)): + """ + Initialize the reversed y-axis scale. + + Parameters: + name (str, optional): Axis title. + breaks (list, optional): Tick positions. + labels (list or callable, optional): Labels for breaks. + limits (tuple, optional): Axis limits (min, max). + expand (tuple): Expansion factor (mult, add). Default is (0.05, 0). + """ + self.name = name + self.breaks = breaks + self.labels = labels + self.limits = limits + self.expand = expand + + def _apply_expansion(self, limits): + """Apply expansion to limits.""" + if limits is None or self.expand is None: + return limits + + low, high = limits + data_range = high - low + + mult = self.expand[0] + add = self.expand[1] if len(self.expand) > 1 else 0 + + new_low = low - data_range * mult - add + new_high = high + data_range * mult + add + return [new_low, new_high] + + def apply(self, fig): + """ + Apply reversed transformation to the y-axis. + + Parameters: + fig (Figure): Plotly figure object. + """ + yaxis_update = {"autorange": "reversed"} + + if self.name is not None: + yaxis_update["title_text"] = self.name + + if self.limits is not None: + expanded_limits = self._apply_expansion(self.limits) + # For reversed axis, we need to swap the order + yaxis_update["range"] = [expanded_limits[1], expanded_limits[0]] + # Remove autorange since we're setting explicit range + yaxis_update.pop("autorange") + + if self.breaks is not None: + yaxis_update["tickmode"] = "array" + yaxis_update["tickvals"] = self.breaks + if self.labels is not None: + if callable(self.labels): + yaxis_update["ticktext"] = self.labels(self.breaks) + else: + yaxis_update["ticktext"] = self.labels + + fig.update_yaxes(**yaxis_update) diff --git a/ggplotly/stats/stat_base.py b/ggplotly/stats/stat_base.py index 1d242aa..904a951 100644 --- a/ggplotly/stats/stat_base.py +++ b/ggplotly/stats/stat_base.py @@ -8,6 +8,21 @@ class Stat: Stats transform data before it is rendered by a geom. For example, stat_count counts the number of observations in each group. + Subclasses must implement the `compute()` method which transforms data + and returns a tuple of (transformed_data, mapping_updates). + + Return Type Contract: + The compute() method MUST return a tuple of: + - data (DataFrame): The transformed data + - mapping (dict): Updated aesthetic mappings (e.g., {'y': 'count'}) + + Example implementation: + def compute(self, data): + # Transform data + result = data.groupby('x').size().reset_index(name='count') + # Return data and any mapping updates + return result, {'y': 'count'} + Parameters: data (DataFrame, optional): Data to use for this stat. mapping (dict, optional): Aesthetic mappings. @@ -62,14 +77,28 @@ def __radd__(self, other): def compute(self, data): """ - Computes the stat, modifying the data as needed. + Compute the statistical transformation on the data. + + This method must be implemented by all stat subclasses. Parameters: - data (DataFrame): The input data. + data (DataFrame): The input data to transform. Returns: - DataFrame: Modified data. + tuple: A tuple of (transformed_data, mapping_updates) where: + - transformed_data (DataFrame): The transformed data + - mapping_updates (dict): Dictionary of aesthetic mapping updates + (e.g., {'y': 'count'} to update the y aesthetic) + + Raises: + NotImplementedError: If not implemented by subclass. + + Example: + >>> class stat_example(Stat): + ... def compute(self, data): + ... result = data.groupby('x').mean().reset_index() + ... return result, {'y': 'mean_value'} """ raise NotImplementedError( - "compute_stat method must be implemented in subclasses." + "compute() method must be implemented in subclasses." ) diff --git a/ggplotly/stats/stat_count.py b/ggplotly/stats/stat_count.py index ea7f302..42f6259 100644 --- a/ggplotly/stats/stat_count.py +++ b/ggplotly/stats/stat_count.py @@ -47,19 +47,23 @@ def compute(self, data): grouping = list(set([v for k, v in self.mapping.items()])) grouping = [g for g in grouping if g in data.columns] - list(set([k for k, v in self.mapping.items()])) - # if x XOR y in grouping - # if ("x" in grouping_keys) ^ ("y" in grouping_keys): - if len(data[grouping].columns) == 1: + # Get the actual column names mapped to x and y + x_col = self.mapping.get('x') + y_col = self.mapping.get('y') - # if len(data.columns) == 1: + # Use value_counts when we only have one grouping column + # OR when all data columns would be used for grouping (nothing left to count) + non_grouping_cols = [c for c in data.columns if c not in grouping] + + if len(grouping) == 1 or len(non_grouping_cols) == 0: + # Use value_counts - works when grouping by all columns tf = data[grouping].value_counts() else: - # if both x and y are in the grouping, remove y. - # Assume that y is the metric we want to summarize - if ("x" in grouping) & ("y" in grouping): - grouping.remove("y") + # If both x and y columns are in the grouping, remove y + # (y will become the count result) + if x_col in grouping and y_col in grouping: + grouping.remove(y_col) self.mapping.pop("y") tf = data.groupby(grouping).agg(stat).iloc[:, [0]] diff --git a/ggplotly/trace_builders.py b/ggplotly/trace_builders.py index 91a774b..691b652 100644 --- a/ggplotly/trace_builders.py +++ b/ggplotly/trace_builders.py @@ -106,8 +106,12 @@ def __init__(self, fig, plot, data, mapping, style_props, color_targets, # Initialize legend tracking on the figure if not present. # This set tracks which legend groups have been shown to prevent # duplicate legend entries in faceted plots. - if not hasattr(fig, '_shown_legendgroups'): - fig._shown_legendgroups = set() + # Note: We use a namespaced attribute to avoid conflicts with Plotly internals. + # This is stored on the figure because legend state must persist across + # multiple geom draws in the same plot. + self._legendgroups_attr = '_ggplotly_shown_legendgroups' + if not hasattr(fig, self._legendgroups_attr): + setattr(fig, self._legendgroups_attr, set()) # Respect the showlegend parameter from geom params self.base_showlegend = params.get("showlegend", True) @@ -129,11 +133,13 @@ def should_show_legend(self, legendgroup): # If showlegend=False was set on the geom, never show legend if not self.base_showlegend: return False + # Get the shown groups set from the figure + shown_groups = getattr(self.fig, self._legendgroups_attr) # If we've already shown this group, don't show again - if legendgroup in self.fig._shown_legendgroups: + if legendgroup in shown_groups: return False # Mark this group as shown and return True - self.fig._shown_legendgroups.add(legendgroup) + shown_groups.add(legendgroup) return True @abstractmethod @@ -409,13 +415,30 @@ class ContinuousColorTraceBuilder(TraceBuilder): with a colorscale instead of separate traces per category. Plotly handles the color interpolation via marker.color and marker.colorscale. + For line-based traces (mode='lines'), we use a segment-based approach + since Plotly's line.color only accepts single values, not arrays. + Example: ggplot(df, aes(x='x', y='y', color='temperature')) + geom_point() # Single trace with colorscale from low to high temperature """ + # Default Viridis colorscale endpoints + DEFAULT_COLORSCALE = [[0, '#440154'], [1, '#fde725']] + def build(self, apply_color_targets_fn): - """Build a single trace with colorscale for continuous color.""" + """Build trace(s) with colorscale for continuous color.""" + # Check if this is a line-based trace + # For lines, we need segment-based rendering since line.color + # only accepts a single value, not an array + if self.payload.get('mode') == 'lines': + return self._build_line_gradient() + + # Original marker-based approach for scatter, bar, etc. + return self._build_marker_gradient() + + def _build_marker_gradient(self): + """Build a single trace with marker colorscale (original approach).""" style_props = self.style_props # Get the numeric color values @@ -457,6 +480,110 @@ def build(self, apply_color_targets_fn): col=self.col, ) + def _build_line_gradient(self): + """ + Build gradient line using individual colored segments. + + Since Plotly's line.color only accepts a single value (not an array), + we draw each segment as a separate Scattergl trace with its own color. + Uses WebGL for efficient rendering of many traces. + """ + import plotly.graph_objects as go + + style_props = self.style_props + + # Get the numeric color values + if style_props.get('color_is_continuous'): + color_values = style_props['color_series'] + else: + color_values = style_props['fill_series'] + + # Get line width from style_props or params + line_width = style_props.get('size', 2) + if line_width is None: + line_width = self.params.get('size', 2) + + # Extract arrays + x_vals = self.x.values if hasattr(self.x, 'values') else list(self.x) + y_vals = self.y.values if hasattr(self.y, 'values') else list(self.y) + c_vals = color_values.values if hasattr(color_values, 'values') else list(color_values) + + vmin, vmax = min(c_vals), max(c_vals) + colorscale = self.DEFAULT_COLORSCALE + + # Draw each segment with interpolated color + for i in range(len(x_vals) - 1): + # Normalize color value at midpoint of segment + t_norm = ((c_vals[i] + c_vals[i + 1]) / 2 - vmin) / (vmax - vmin) if vmax != vmin else 0 + color = self._interpolate_color(colorscale, t_norm) + + self.fig.add_trace( + go.Scattergl( # WebGL for performance with many traces + x=[x_vals[i], x_vals[i + 1]], + y=[y_vals[i], y_vals[i + 1]], + mode='lines', + line=dict(color=color, width=line_width), + opacity=self.alpha, + showlegend=False, + hoverinfo='skip', + # Tag for scale_color_gradient to update colors + meta={'_ggplotly_line_gradient': True, '_color_norm': t_norm} + ), + row=self.row, + col=self.col, + ) + + # Add invisible trace for colorbar + self.fig.add_trace( + go.Scatter( + x=[None], + y=[None], + mode='markers', + marker=dict( + color=[vmin, vmax], + colorscale=colorscale, + showscale=True, + colorbar=dict(title=self.mapping.get('color', '')) + ), + showlegend=False, + hoverinfo='skip' + ), + row=self.row, + col=self.col, + ) + + @staticmethod + def _interpolate_color(colorscale, t): + """ + Interpolate between colorscale endpoints. + + Parameters: + colorscale: List of [position, color] pairs (e.g., [[0, '#440154'], [1, '#fde725']]) + t: Normalized value between 0 and 1 + + Returns: + str: Interpolated RGB color string + """ + t = max(0, min(1, t)) # Clamp to [0, 1] + + low_color = colorscale[0][1] + high_color = colorscale[1][1] + + # Parse hex colors to RGB + def hex_to_rgb(hex_color): + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4)) + + low_rgb = hex_to_rgb(low_color) + high_rgb = hex_to_rgb(high_color) + + # Linear interpolation + r = int(low_rgb[0] + t * (high_rgb[0] - low_rgb[0])) + g = int(low_rgb[1] + t * (high_rgb[1] - low_rgb[1])) + b = int(low_rgb[2] + t * (high_rgb[2] - low_rgb[2])) + + return f'rgb({r}, {g}, {b})' + class SingleTraceBuilder(TraceBuilder): """ diff --git a/ggplotly/utils.py b/ggplotly/utils.py index 5c98637..ccfaef7 100644 --- a/ggplotly/utils.py +++ b/ggplotly/utils.py @@ -82,8 +82,12 @@ def apply(self, plot): def save_html(self, plot): """ Save the plot as an HTML file. + + Uses CDN references for Plotly.js and MathJax, resulting in small file + sizes (~8KB) but requiring internet for viewing. + MathJax CDN enables LaTeX rendering (e.g., geom_text with parse=True). """ - plot.fig.write_html(self.filename) + plot.fig.write_html(self.filename, include_plotlyjs='cdn', include_mathjax='cdn') print(f"Plot saved as HTML: {self.filename}") def save_png(self, plot, width, height): diff --git a/pyproject.toml b/pyproject.toml index a68f2d7..143969f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ggplotly" -version = "0.3.5" +version = "0.4.0" description = "A grammar of graphics for Python built on Plotly" readme = "README.md" license = {text = "MIT"} @@ -83,7 +83,7 @@ Homepage = "https://github.com/bbcho/ggplotly" Documentation = "https://bbcho.github.io/ggplotly" Repository = "https://github.com/bbcho/ggplotly" Issues = "https://github.com/bbcho/ggplotly/issues" -Changelog = "https://github.com/bbcho/ggplotly/blob/main/CHANGELOG.md" +Roadmap = "https://github.com/bbcho/ggplotly/blob/main/ROADMAP.md" [tool.setuptools.packages.find] where = ["."] @@ -93,7 +93,7 @@ include = ["ggplotly*"] ggplotly = ["data/*.csv"] [tool.pytest.ini_options] -testpaths = ["pytest", "docs"] +testpaths = ["pytest", "examples", "docs"] python_files = ["test_*.py"] addopts = "-v --tb=short --nbmake" diff --git a/pytest/test_geom_acf.py b/pytest/test_geom_acf.py index 1629f82..b0dd0ff 100644 --- a/pytest/test_geom_acf.py +++ b/pytest/test_geom_acf.py @@ -44,10 +44,12 @@ def test_custom_color(self): def test_missing_y_raises(self): """Test that missing y aesthetic raises error.""" + from ggplotly.exceptions import RequiredAestheticError + df = pd.DataFrame({'x': range(100), 'value': np.random.randn(100)}) plot = ggplot(df, aes(x='x')) + geom_acf() - with pytest.raises(ValueError, match="requires y aesthetic"): + with pytest.raises(RequiredAestheticError, match="requires aesthetics.*y"): plot.draw() def test_starts_at_lag_one(self): diff --git a/pytest/test_geom_candlestick.py b/pytest/test_geom_candlestick.py index f85622e..0dd1439 100644 --- a/pytest/test_geom_candlestick.py +++ b/pytest/test_geom_candlestick.py @@ -102,14 +102,18 @@ def test_candlestick_x_dates_preserved(self, ohlc_data): assert len(fig.data[0].x) == len(ohlc_data) def test_missing_aesthetic_raises_error(self, ohlc_data): - """Test that missing required aesthetic raises ValueError.""" - with pytest.raises(ValueError, match="geom_candlestick requires aesthetics"): + """Test that missing required aesthetic raises RequiredAestheticError.""" + from ggplotly.exceptions import RequiredAestheticError + + with pytest.raises(RequiredAestheticError, match="geom_candlestick requires aesthetics"): p = ggplot(ohlc_data, aes(x='date', open='open', high='high')) + geom_candlestick() p.draw() def test_missing_multiple_aesthetics_lists_all(self, ohlc_data): """Test error message lists all missing aesthetics.""" - with pytest.raises(ValueError, match="Missing:.*low.*close"): + from ggplotly.exceptions import RequiredAestheticError + + with pytest.raises(RequiredAestheticError, match="low.*close"): p = ggplot(ohlc_data, aes(x='date', open='open', high='high')) + geom_candlestick() p.draw() @@ -271,8 +275,10 @@ def test_ohlc_data_matches_input(self, small_ohlc_data): assert len(trace.close) == 5 def test_ohlc_missing_aesthetic_raises_error(self, ohlc_data): - """Test that missing required aesthetic raises ValueError.""" - with pytest.raises(ValueError, match="geom_ohlc requires aesthetics"): + """Test that missing required aesthetic raises RequiredAestheticError.""" + from ggplotly.exceptions import RequiredAestheticError + + with pytest.raises(RequiredAestheticError, match="geom_ohlc requires aesthetics"): p = ggplot(ohlc_data, aes(x='date', open='open')) + geom_ohlc() p.draw() diff --git a/pytest/test_geom_norm.py b/pytest/test_geom_norm.py index c58f4ce..4a611b3 100644 --- a/pytest/test_geom_norm.py +++ b/pytest/test_geom_norm.py @@ -67,11 +67,13 @@ def test_auto_fit(self): def test_missing_x_raises(self): """Test that missing x aesthetic raises error.""" + from ggplotly.exceptions import ColumnNotFoundError + df = pd.DataFrame({'value': np.random.randn(100)}) # x mapped to non-existent column plot = ggplot(df, aes(x='nonexistent')) + geom_norm() - with pytest.raises(ValueError, match="requires x aesthetic"): + with pytest.raises(ColumnNotFoundError, match="nonexistent.*not found"): plot.draw() def test_linetype(self): diff --git a/pytest/test_geom_pacf.py b/pytest/test_geom_pacf.py index 11cbe3c..95c708d 100644 --- a/pytest/test_geom_pacf.py +++ b/pytest/test_geom_pacf.py @@ -44,10 +44,12 @@ def test_custom_color(self): def test_missing_y_raises(self): """Test that missing y aesthetic raises error.""" + from ggplotly.exceptions import RequiredAestheticError + df = pd.DataFrame({'x': range(100), 'value': np.random.randn(100)}) plot = ggplot(df, aes(x='x')) + geom_pacf() - with pytest.raises(ValueError, match="requires y aesthetic"): + with pytest.raises(RequiredAestheticError, match="requires aesthetics.*y"): plot.draw() def test_starts_at_lag_one(self): diff --git a/pytest/test_geom_point_3d.py b/pytest/test_geom_point_3d.py index 535ce5c..132f7bb 100644 --- a/pytest/test_geom_point_3d.py +++ b/pytest/test_geom_point_3d.py @@ -102,8 +102,10 @@ def test_alpha_parameter(self, sample_3d_data): assert fig.data[0].marker.opacity == 0.5, "Marker opacity should be 0.5" def test_missing_z_raises_error(self, sample_3d_data): - """Test that missing z aesthetic raises ValueError.""" - with pytest.raises(ValueError, match="geom_point_3d requires 'x', 'y', and 'z' aesthetics"): + """Test that missing z aesthetic raises RequiredAestheticError.""" + from ggplotly.exceptions import RequiredAestheticError + + with pytest.raises(RequiredAestheticError, match="geom_point_3d requires aesthetics"): p = ggplot(sample_3d_data, aes(x='x', y='y')) + geom_point_3d() p.draw() diff --git a/pytest/test_geom_range.py b/pytest/test_geom_range.py index e6eb6e7..44450fe 100644 --- a/pytest/test_geom_range.py +++ b/pytest/test_geom_range.py @@ -270,8 +270,10 @@ def test_sparse_data(self): def test_missing_aesthetic(self): """Test error when missing required aesthetic.""" + from ggplotly.exceptions import RequiredAestheticError + df = pd.DataFrame({'date': [1, 2, 3], 'value': [1, 2, 3]}) - with pytest.raises(ValueError): + with pytest.raises(RequiredAestheticError): p = ggplot(df, aes(x='date')) + geom_range() fig = p.draw() diff --git a/pytest/test_geom_rect_label.py b/pytest/test_geom_rect_label.py new file mode 100644 index 0000000..dd16528 --- /dev/null +++ b/pytest/test_geom_rect_label.py @@ -0,0 +1,683 @@ +""" +Tests for geom_rect and geom_label. + +Covers all four test categories: +1. Basic functionality tests +2. Edge case tests +3. Integration tests (faceting & color mappings) +4. Visual regression tests +""" + +import pandas as pd +import pytest + +from ggplotly import ( + aes, + facet_grid, + facet_wrap, + geom_label, + geom_line, + geom_point, + geom_rect, + geom_text, + ggplot, +) + + +# ============================================================================= +# GEOM_RECT TESTS +# ============================================================================= + + +class TestGeomRectBasic: + """Basic functionality tests for geom_rect.""" + + def test_rect_basic(self): + """Test basic rectangle creation.""" + df = pd.DataFrame({ + "xmin": [1], "xmax": [3], "ymin": [1], "ymax": [4] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect() + fig = plot.draw() + assert len(fig.data) >= 1 + # Check it's a filled scatter trace + assert fig.data[0].fill == "toself" + + def test_rect_multiple(self): + """Test multiple rectangles.""" + df = pd.DataFrame({ + "xmin": [1, 4], "xmax": [3, 6], + "ymin": [1, 2], "ymax": [4, 5] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect() + fig = plot.draw() + # Should have 2 traces (one per rectangle, grouped in legendgroup) + assert len(fig.data) == 2 + + def test_rect_with_fill_color(self): + """Test rectangle with explicit fill color.""" + df = pd.DataFrame({ + "xmin": [1], "xmax": [3], "ymin": [1], "ymax": [4] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect( + fill="red" + ) + fig = plot.draw() + assert fig.data[0].fillcolor == "red" + + def test_rect_with_border(self): + """Test rectangle with border color and width.""" + df = pd.DataFrame({ + "xmin": [1], "xmax": [3], "ymin": [1], "ymax": [4] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect( + fill="lightblue", color="navy", size=2 + ) + fig = plot.draw() + assert fig.data[0].line.color == "navy" + assert fig.data[0].line.width == 2 + + def test_rect_alpha(self): + """Test rectangle transparency.""" + df = pd.DataFrame({ + "xmin": [1], "xmax": [3], "ymin": [1], "ymax": [4] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect( + alpha=0.3 + ) + fig = plot.draw() + assert fig.data[0].opacity == 0.3 + + def test_rect_default_alpha(self): + """Test rectangle default transparency is 0.5.""" + df = pd.DataFrame({ + "xmin": [1], "xmax": [3], "ymin": [1], "ymax": [4] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect() + fig = plot.draw() + assert fig.data[0].opacity == 0.5 + + +class TestGeomRectEdgeCases: + """Edge case tests for geom_rect.""" + + def test_rect_empty_dataframe(self): + """Test rect with empty DataFrame doesn't crash.""" + df = pd.DataFrame({ + "xmin": [], "xmax": [], "ymin": [], "ymax": [] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect() + fig = plot.draw() # Should not raise + assert len(fig.data) == 0 + + def test_rect_zero_area(self): + """Test rectangle with zero area (point).""" + df = pd.DataFrame({ + "xmin": [1], "xmax": [1], "ymin": [1], "ymax": [1] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect() + fig = plot.draw() # Should not crash + assert len(fig.data) >= 1 + + def test_rect_negative_coords(self): + """Test rectangle with negative coordinates.""" + df = pd.DataFrame({ + "xmin": [-5], "xmax": [-1], "ymin": [-3], "ymax": [-1] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect() + fig = plot.draw() + # Verify the path includes negative values + assert min(fig.data[0].x) == -5 + assert min(fig.data[0].y) == -3 + + def test_rect_large_values(self): + """Test rectangle with large coordinate values.""" + df = pd.DataFrame({ + "xmin": [1e6], "xmax": [1e7], "ymin": [1e6], "ymax": [1e7] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect() + fig = plot.draw() + assert len(fig.data) >= 1 + + def test_rect_inverted_coords(self): + """Test rectangle where xmin > xmax (inverted).""" + df = pd.DataFrame({ + "xmin": [5], "xmax": [1], "ymin": [5], "ymax": [1] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect() + fig = plot.draw() # Should still work (creates inverted rectangle) + assert len(fig.data) >= 1 + + def test_rect_linetype_dash(self): + """Test rectangle with dashed border.""" + df = pd.DataFrame({ + "xmin": [1], "xmax": [3], "ymin": [1], "ymax": [4] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect( + color="black", linetype="dash" + ) + fig = plot.draw() + assert fig.data[0].line.dash == "dash" + + +class TestGeomRectIntegration: + """Integration tests for geom_rect with faceting and aesthetics.""" + + def test_rect_with_fill_aesthetic(self): + """Test rectangle with fill mapped to variable.""" + df = pd.DataFrame({ + "xmin": [1, 4], "xmax": [3, 6], + "ymin": [1, 2], "ymax": [4, 5], + "category": ["A", "B"] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", fill="category")) + geom_rect() + fig = plot.draw() + # Should have separate traces for each category + assert len(fig.data) == 2 + # Check they have different colors + colors = {fig.data[0].fillcolor, fig.data[1].fillcolor} + assert len(colors) == 2 + + def test_rect_with_color_aesthetic(self): + """Test rectangle with color mapped to variable.""" + df = pd.DataFrame({ + "xmin": [1, 4], "xmax": [3, 6], + "ymin": [1, 2], "ymax": [4, 5], + "category": ["A", "B"] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", color="category")) + geom_rect() + fig = plot.draw() + assert len(fig.data) == 2 + + def test_rect_with_facet_wrap(self): + """Test rectangle with facet_wrap.""" + df = pd.DataFrame({ + "xmin": [1, 1], "xmax": [3, 3], + "ymin": [1, 1], "ymax": [4, 4], + "panel": ["A", "B"] + }) + plot = ( + ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + + geom_rect() + + facet_wrap("panel") + ) + fig = plot.draw() + # Should create subplot structure + assert len(fig.data) >= 2 + + def test_rect_with_facet_grid(self): + """Test rectangle with facet_grid.""" + df = pd.DataFrame({ + "xmin": [1, 1, 1, 1], "xmax": [3, 3, 3, 3], + "ymin": [1, 1, 1, 1], "ymax": [4, 4, 4, 4], + "row_var": ["R1", "R1", "R2", "R2"], + "col_var": ["C1", "C2", "C1", "C2"] + }) + plot = ( + ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + + geom_rect() + + facet_grid(rows="row_var", cols="col_var") + ) + fig = plot.draw() + assert len(fig.data) >= 4 + + def test_rect_overlay_on_scatter(self): + """Test rectangle as overlay on scatter plot.""" + scatter_df = pd.DataFrame({ + "x": [1, 2, 3, 4, 5], + "y": [2, 4, 3, 5, 4] + }) + rect_df = pd.DataFrame({ + "xmin": [2], "xmax": [4], "ymin": [2.5], "ymax": [4.5] + }) + plot = ( + ggplot(scatter_df, aes(x="x", y="y")) + + geom_rect(data=rect_df, mapping=aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"), + fill="yellow", alpha=0.3) + + geom_point() + ) + fig = plot.draw() + # Should have both rect and point traces + assert len(fig.data) >= 2 + + +# ============================================================================= +# GEOM_LABEL TESTS +# ============================================================================= + + +class TestGeomLabelBasic: + """Basic functionality tests for geom_label.""" + + def test_label_basic(self): + """Test basic label creation.""" + df = pd.DataFrame({ + "x": [1, 2, 3], + "y": [1, 2, 3], + "label": ["A", "B", "C"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label() + fig = plot.draw() + # Labels are added as annotations + assert len(fig.layout.annotations) == 3 + + def test_label_text_content(self): + """Test label text content is correct.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Hello World"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label() + fig = plot.draw() + assert fig.layout.annotations[0].text == "Hello World" + + def test_label_fill_color(self): + """Test label background fill color.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(fill="lightblue") + fig = plot.draw() + assert fig.layout.annotations[0].bgcolor == "lightblue" + + def test_label_text_color(self): + """Test label text color.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(color="red") + fig = plot.draw() + assert fig.layout.annotations[0].font.color == "red" + + def test_label_font_size(self): + """Test label font size.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(size=16) + fig = plot.draw() + assert fig.layout.annotations[0].font.size == 16 + + def test_label_default_fill_white(self): + """Test label default fill is white.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label() + fig = plot.draw() + assert fig.layout.annotations[0].bgcolor == "white" + + def test_label_alpha(self): + """Test label background transparency.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(alpha=0.5) + fig = plot.draw() + assert fig.layout.annotations[0].opacity == 0.5 + + +class TestGeomLabelEdgeCases: + """Edge case tests for geom_label.""" + + def test_label_empty_dataframe(self): + """Test label with empty DataFrame doesn't crash.""" + df = pd.DataFrame({ + "x": [], + "y": [], + "label": [] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label() + fig = plot.draw() # Should not raise + assert len(fig.layout.annotations) == 0 + + def test_label_numeric_labels(self): + """Test label with numeric values.""" + df = pd.DataFrame({ + "x": [1, 2, 3], + "y": [1, 2, 3], + "label": [100, 200, 300] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label() + fig = plot.draw() + assert fig.layout.annotations[0].text == "100" + + def test_label_special_characters(self): + """Test label with special characters.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Hello & 'Friends'"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label() + fig = plot.draw() + assert len(fig.layout.annotations) == 1 + + def test_label_unicode(self): + """Test label with unicode characters.""" + df = pd.DataFrame({ + "x": [1, 2], + "y": [1, 2], + "label": ["Hello", "World"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label() + fig = plot.draw() + assert len(fig.layout.annotations) == 2 + + def test_label_na_rm_true(self): + """Test label with na_rm=True removes NA values.""" + df = pd.DataFrame({ + "x": [1, 2, 3], + "y": [1, None, 3], + "label": ["A", "B", "C"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(na_rm=True) + fig = plot.draw() + # Should only have 2 labels (row with None y is removed) + assert len(fig.layout.annotations) == 2 + + def test_label_nudge_x(self): + """Test label horizontal nudge.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(nudge_x=0.5) + fig = plot.draw() + assert fig.layout.annotations[0].x == 1.5 + + def test_label_nudge_y(self): + """Test label vertical nudge.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(nudge_y=0.5) + fig = plot.draw() + assert fig.layout.annotations[0].y == 1.5 + + def test_label_hjust_left(self): + """Test label horizontal justification left.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(hjust=0) + fig = plot.draw() + assert fig.layout.annotations[0].xanchor == "left" + + def test_label_hjust_right(self): + """Test label horizontal justification right.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(hjust=1) + fig = plot.draw() + assert fig.layout.annotations[0].xanchor == "right" + + def test_label_vjust_top(self): + """Test label vertical justification top.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(vjust=1) + fig = plot.draw() + assert fig.layout.annotations[0].yanchor == "top" + + def test_label_vjust_bottom(self): + """Test label vertical justification bottom.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(vjust=0) + fig = plot.draw() + assert fig.layout.annotations[0].yanchor == "bottom" + + def test_label_parse_latex(self): + """Test label with parse=True for LaTeX.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["\\alpha"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label(parse=True) + fig = plot.draw() + assert fig.layout.annotations[0].text == "$\\alpha$" + + +class TestGeomLabelIntegration: + """Integration tests for geom_label with faceting and aesthetics.""" + + def test_label_with_fill_aesthetic(self): + """Test label with fill mapped to variable.""" + df = pd.DataFrame({ + "x": [1, 2], + "y": [1, 2], + "label": ["A", "B"], + "category": ["Cat1", "Cat2"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label", fill="category")) + geom_label() + fig = plot.draw() + # Annotations should have different background colors + colors = {fig.layout.annotations[0].bgcolor, fig.layout.annotations[1].bgcolor} + assert len(colors) == 2 + + def test_label_with_points(self): + """Test label combined with geom_point.""" + df = pd.DataFrame({ + "x": [1, 2, 3], + "y": [1, 2, 3], + "label": ["A", "B", "C"] + }) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + geom_label(aes(label="label"), nudge_y=0.2) + ) + fig = plot.draw() + # Should have point trace and annotations + assert len(fig.data) >= 1 # Points + assert len(fig.layout.annotations) == 3 # Labels + + def test_label_with_line(self): + """Test label combined with geom_line.""" + df = pd.DataFrame({ + "x": [1, 2, 3], + "y": [1, 2, 3], + "label": ["Start", "Mid", "End"] + }) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_line() + + geom_label(aes(label="label")) + ) + fig = plot.draw() + assert len(fig.layout.annotations) == 3 + + def test_label_with_facet_wrap(self): + """Test label with facet_wrap.""" + df = pd.DataFrame({ + "x": [1, 1], + "y": [1, 1], + "label": ["A", "B"], + "panel": ["P1", "P2"] + }) + plot = ( + ggplot(df, aes(x="x", y="y", label="label")) + + geom_label() + + facet_wrap("panel") + ) + fig = plot.draw() + # Should create annotations in faceted structure + assert len(fig.layout.annotations) >= 2 + + def test_label_vs_text(self): + """Test that geom_label differs from geom_text by having background.""" + df = pd.DataFrame({ + "x": [1], + "y": [1], + "label": ["Test"] + }) + + # geom_label should use annotations with bgcolor + label_plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label() + label_fig = label_plot.draw() + + # geom_text should use scatter trace with text mode + text_plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_text() + text_fig = text_plot.draw() + + # Label uses annotations, text uses scatter traces + assert len(label_fig.layout.annotations) == 1 + assert label_fig.layout.annotations[0].bgcolor is not None + + +# ============================================================================= +# VISUAL REGRESSION TESTS +# ============================================================================= + + +class TestVisualRegressionRect: + """Visual regression tests for geom_rect.""" + + def get_figure_signature(self, fig): + """Extract key properties from figure for comparison.""" + signature = { + "num_traces": len(fig.data), + "traces": [] + } + for trace in fig.data: + trace_sig = { + "type": trace.type, + "fill": getattr(trace, "fill", None), + "fillcolor": getattr(trace, "fillcolor", None), + "opacity": getattr(trace, "opacity", None), + } + if hasattr(trace, "line") and trace.line: + trace_sig["line"] = { + "color": getattr(trace.line, "color", None), + "width": getattr(trace.line, "width", None), + "dash": getattr(trace.line, "dash", None), + } + signature["traces"].append(trace_sig) + return signature + + def test_rect_visual_signature(self): + """Test that rect produces expected visual signature.""" + df = pd.DataFrame({ + "xmin": [1], "xmax": [3], "ymin": [1], "ymax": [4] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect( + fill="blue", color="red", size=2, alpha=0.4 + ) + fig = plot.draw() + sig = self.get_figure_signature(fig) + + assert sig["num_traces"] == 1 + assert sig["traces"][0]["fill"] == "toself" + assert sig["traces"][0]["fillcolor"] == "blue" + assert sig["traces"][0]["opacity"] == 0.4 + assert sig["traces"][0]["line"]["color"] == "red" + assert sig["traces"][0]["line"]["width"] == 2 + + def test_rect_path_signature(self): + """Test that rect creates correct rectangular path.""" + df = pd.DataFrame({ + "xmin": [1], "xmax": [3], "ymin": [2], "ymax": [5] + }) + plot = ggplot(df, aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax")) + geom_rect() + fig = plot.draw() + + # Check the path forms a closed rectangle + x_path = list(fig.data[0].x) + y_path = list(fig.data[0].y) + + # Should be 5 points (closed rectangle) + assert len(x_path) == 5 + assert len(y_path) == 5 + + # Path should include all corners + assert 1 in x_path # xmin + assert 3 in x_path # xmax + assert 2 in y_path # ymin + assert 5 in y_path # ymax + + +class TestVisualRegressionLabel: + """Visual regression tests for geom_label.""" + + def get_annotation_signature(self, fig): + """Extract key properties from annotations.""" + return [ + { + "text": ann.text, + "x": ann.x, + "y": ann.y, + "bgcolor": ann.bgcolor, + "font_color": ann.font.color if ann.font else None, + "font_size": ann.font.size if ann.font else None, + "xanchor": ann.xanchor, + "yanchor": ann.yanchor, + "opacity": ann.opacity, + } + for ann in fig.layout.annotations + ] + + def test_label_visual_signature(self): + """Test that label produces expected visual signature.""" + df = pd.DataFrame({ + "x": [1], + "y": [2], + "label": ["Test"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label( + fill="yellow", color="blue", size=14, alpha=0.9 + ) + fig = plot.draw() + sig = self.get_annotation_signature(fig) + + assert len(sig) == 1 + assert sig[0]["text"] == "Test" + assert sig[0]["x"] == 1 + assert sig[0]["y"] == 2 + assert sig[0]["bgcolor"] == "yellow" + assert sig[0]["font_color"] == "blue" + assert sig[0]["font_size"] == 14 + assert sig[0]["opacity"] == 0.9 + + def test_label_nudge_signature(self): + """Test label nudge visual signature.""" + df = pd.DataFrame({ + "x": [5], + "y": [10], + "label": ["Nudged"] + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_label( + nudge_x=1, nudge_y=2 + ) + fig = plot.draw() + sig = self.get_annotation_signature(fig) + + # Position should be nudged + assert sig[0]["x"] == 6 # 5 + 1 + assert sig[0]["y"] == 12 # 10 + 2 diff --git a/pytest/test_geom_sankey.py b/pytest/test_geom_sankey.py index 6d0f5b7..3fa5b30 100644 --- a/pytest/test_geom_sankey.py +++ b/pytest/test_geom_sankey.py @@ -154,7 +154,9 @@ def test_link_indices_are_correct(self): assert labels[tgt_idx] == expected_target def test_missing_aesthetics_raises(self): - """Test that missing required aesthetics raises ValueError.""" + """Test that missing required aesthetics raises RequiredAestheticError.""" + from ggplotly.exceptions import RequiredAestheticError + df = pd.DataFrame({ 'source': ['A', 'B'], 'target': ['X', 'X'], @@ -164,13 +166,13 @@ def test_missing_aesthetics_raises(self): # Missing value plot = (ggplot(df, aes(source='source', target='target')) + geom_sankey()) - with pytest.raises(ValueError, match="Missing"): + with pytest.raises(RequiredAestheticError, match="value"): plot.draw() # Missing source plot2 = (ggplot(df, aes(target='target', value='value')) + geom_sankey()) - with pytest.raises(ValueError, match="Missing"): + with pytest.raises(RequiredAestheticError, match="source"): plot2.draw() def test_missing_column_raises(self): diff --git a/pytest/test_geom_surface.py b/pytest/test_geom_surface.py index f797039..52794bc 100644 --- a/pytest/test_geom_surface.py +++ b/pytest/test_geom_surface.py @@ -120,8 +120,10 @@ def test_surface_x_y_are_1d_arrays(self, surface_data): assert len(y_data) == 30, "Y should have 30 unique values" def test_missing_z_raises_error(self, surface_data): - """Test that missing z aesthetic raises ValueError.""" - with pytest.raises(ValueError, match="geom_surface requires 'x', 'y', and 'z' aesthetics"): + """Test that missing z aesthetic raises RequiredAestheticError.""" + from ggplotly.exceptions import RequiredAestheticError + + with pytest.raises(RequiredAestheticError, match="geom_surface requires aesthetics"): p = ggplot(surface_data, aes(x='x', y='y')) + geom_surface() p.draw() @@ -457,8 +459,10 @@ def test_wireframe_alpha(self, surface_data): assert fig.data[0].opacity == 0.5, "Opacity should be 0.5" def test_wireframe_missing_z_raises_error(self, surface_data): - """Test that missing z aesthetic raises ValueError.""" - with pytest.raises(ValueError, match="geom_wireframe requires 'x', 'y', and 'z' aesthetics"): + """Test that missing z aesthetic raises RequiredAestheticError.""" + from ggplotly.exceptions import RequiredAestheticError + + with pytest.raises(RequiredAestheticError, match="geom_wireframe requires aesthetics"): p = ggplot(surface_data, aes(x='x', y='y')) + geom_wireframe() p.draw() diff --git a/pytest/test_geom_waterfall.py b/pytest/test_geom_waterfall.py index f52a192..f65853b 100644 --- a/pytest/test_geom_waterfall.py +++ b/pytest/test_geom_waterfall.py @@ -217,7 +217,9 @@ def test_text_labels_present(self): assert '-30' in text_values def test_missing_y_aesthetic_raises(self): - """Test that missing y aesthetic raises ValueError.""" + """Test that missing y aesthetic raises RequiredAestheticError.""" + from ggplotly.exceptions import RequiredAestheticError + df = pd.DataFrame({ 'category': ['A', 'B', 'C'], 'value': [100, 50, -30] @@ -226,11 +228,13 @@ def test_missing_y_aesthetic_raises(self): # Missing y plot = (ggplot(df, aes(x='category')) + geom_waterfall()) - with pytest.raises(ValueError, match="Missing"): + with pytest.raises(RequiredAestheticError, match="y"): plot.draw() def test_missing_x_aesthetic_raises(self): - """Test that missing x aesthetic raises ValueError.""" + """Test that missing x aesthetic raises RequiredAestheticError.""" + from ggplotly.exceptions import RequiredAestheticError + df = pd.DataFrame({ 'category': ['A', 'B', 'C'], 'value': [100, 50, -30] @@ -238,18 +242,20 @@ def test_missing_x_aesthetic_raises(self): # Create geom with only y mapping (no x) plot = (ggplot(df) + geom_waterfall(mapping=aes(y='value'))) - with pytest.raises(ValueError, match="Missing"): + with pytest.raises(RequiredAestheticError, match="x"): plot.draw() def test_missing_column_raises(self): - """Test that missing data column raises ValueError.""" + """Test that missing data column raises ColumnNotFoundError.""" + from ggplotly.exceptions import ColumnNotFoundError + df = pd.DataFrame({ 'category': ['A', 'B', 'C'], }) plot = (ggplot(df, aes(x='category', y='nonexistent')) + geom_waterfall()) - with pytest.raises(ValueError, match="not found"): + with pytest.raises(ColumnNotFoundError, match="not found"): plot.draw() def test_default_measures(self): diff --git a/pytest/test_geoms.py b/pytest/test_geoms.py index 5d6020d..7b8f4b5 100644 --- a/pytest/test_geoms.py +++ b/pytest/test_geoms.py @@ -602,3 +602,47 @@ def test_original_data_unchanged(self, simple_data): p.draw() pd.testing.assert_frame_equal(simple_data, original) + + +class TestDictInput: + """Tests for dict input support.""" + + def test_ggplot_accepts_dict(self): + """Test that ggplot accepts a dict and converts to DataFrame.""" + data = {'x': [1, 2, 3], 'y': [4, 5, 6]} + p = ggplot(data, aes(x='x', y='y')) + geom_point() + fig = p.draw() + + assert isinstance(fig, Figure) + assert len(fig.data) == 1 + assert list(fig.data[0].x) == [1, 2, 3] + assert list(fig.data[0].y) == [4, 5, 6] + + def test_dict_with_geom_bar(self): + """Test dict input with geom_bar (uses stat_count).""" + np.random.seed(42) + data = {'x': np.random.randint(10, size=100)} + p = ggplot(data, aes(x='x')) + geom_bar() + fig = p.draw() + + assert isinstance(fig, Figure) + assert fig.data[0].type == 'bar' + + def test_dict_with_multiple_geoms(self): + """Test dict input with multiple geoms.""" + np.random.seed(42) + data = {'x': np.random.randint(10, size=100)} + p = ggplot(data, aes(x='x')) + geom_bar() + geom_bar(data, aes(x='x'), name='Second') + fig = p.draw() + + assert isinstance(fig, Figure) + assert len(fig.data) == 2 + + def test_dict_with_numpy_arrays(self): + """Test dict containing numpy arrays.""" + data = {'x': np.array([1, 2, 3, 4, 5]), 'y': np.array([2, 4, 6, 8, 10])} + p = ggplot(data, aes(x='x', y='y')) + geom_line() + fig = p.draw() + + assert isinstance(fig, Figure) + assert len(fig.data[0].x) == 5 diff --git a/pytest/test_new_parameters.py b/pytest/test_new_parameters.py new file mode 100644 index 0000000..4dc62f3 --- /dev/null +++ b/pytest/test_new_parameters.py @@ -0,0 +1,910 @@ +# pytest/test_new_parameters.py +""" +Tests for new ggplot2-compatible parameters added to ggplotly. + +Tests cover: +- geom_point: stroke parameter +- geom_segment: arrow parameter +- geom_errorbar: width parameter +- linewidth alias for size +- geom_text: parse parameter +- New position exports: position_fill, position_nudge, position_identity, position_dodge2 + +Test categories: +1. Basic functionality tests +2. Edge cases +3. Integration tests (with faceting, color mappings, multiple geoms) +4. Visual regression tests (image comparison) +""" + +import hashlib +import json +import os +import tempfile + +import numpy as np +import pandas as pd +import pytest + +from ggplotly import ( + aes, + facet_grid, + facet_wrap, + geom_bar, + geom_density, + geom_errorbar, + geom_line, + geom_point, + geom_ribbon, + geom_segment, + geom_smooth, + geom_text, + ggplot, + labs, + position_dodge, + position_dodge2, + position_fill, + position_identity, + position_jitter, + position_nudge, + scale_color_manual, + theme_minimal, +) + + +# ============================================================================= +# BASIC FUNCTIONALITY TESTS +# ============================================================================= + + +class TestGeomPointStroke: + """Tests for geom_point stroke parameter.""" + + def test_stroke_default(self): + """Test that stroke defaults to 0.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + fig = plot.draw() + assert fig is not None + + def test_stroke_with_value(self): + """Test that stroke parameter is applied.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(stroke=2) + fig = plot.draw() + # Check that marker line width is set + assert fig.data[0].marker.line.width == 2 + + def test_stroke_zero_no_line(self): + """Test that stroke=0 doesn't add marker line.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(stroke=0) + fig = plot.draw() + # stroke=0 should result in empty marker_line (no width set) + assert fig is not None + + +class TestGeomSegmentArrow: + """Tests for geom_segment arrow parameter.""" + + def test_arrow_default_false(self): + """Test that arrow defaults to False.""" + df = pd.DataFrame({"x": [1], "y": [1], "xend": [2], "yend": [2]}) + plot = ggplot(df, aes(x="x", y="y", xend="xend", yend="yend")) + geom_segment() + fig = plot.draw() + # Without arrow, mode should be 'lines' + assert fig.data[0].mode == "lines" + + def test_arrow_true_adds_markers(self): + """Test that arrow=True changes mode to lines+markers.""" + df = pd.DataFrame({"x": [1], "y": [1], "xend": [2], "yend": [2]}) + plot = ggplot(df, aes(x="x", y="y", xend="xend", yend="yend")) + geom_segment( + arrow=True + ) + fig = plot.draw() + # With arrow, mode should be 'lines+markers' + assert fig.data[0].mode == "lines+markers" + # Check arrow marker is configured + assert fig.data[0].marker.symbol[1] == "arrow" + + def test_arrow_size_parameter(self): + """Test that arrow_size parameter is applied.""" + df = pd.DataFrame({"x": [1], "y": [1], "xend": [2], "yend": [2]}) + plot = ggplot(df, aes(x="x", y="y", xend="xend", yend="yend")) + geom_segment( + arrow=True, arrow_size=20 + ) + fig = plot.draw() + assert fig.data[0].marker.size[1] == 20 + + +class TestGeomErrorbarWidth: + """Tests for geom_errorbar width parameter.""" + + def test_width_default(self): + """Test that width has a default value.""" + df = pd.DataFrame({"x": [1, 2], "y": [5, 6], "ymin": [4, 5], "ymax": [6, 7]}) + plot = ggplot(df, aes(x="x", y="y", ymin="ymin", ymax="ymax")) + geom_errorbar() + fig = plot.draw() + # Default width should be 4 + assert fig.data[0].error_y.width == 4 + + def test_width_custom(self): + """Test that custom width is applied.""" + df = pd.DataFrame({"x": [1, 2], "y": [5, 6], "ymin": [4, 5], "ymax": [6, 7]}) + plot = ggplot(df, aes(x="x", y="y", ymin="ymin", ymax="ymax")) + geom_errorbar( + width=10 + ) + fig = plot.draw() + assert fig.data[0].error_y.width == 10 + + +class TestLinewidthAlias: + """Tests for linewidth as alias for size.""" + + def test_linewidth_alias_in_geom_line(self): + """Test that linewidth works as alias for size in geom_line.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_line(linewidth=5) + fig = plot.draw() + # linewidth should map to line width + assert fig.data[0].line.width == 5 + + def test_size_still_works(self): + """Test that size parameter still works.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_line(size=3) + fig = plot.draw() + assert fig.data[0].line.width == 3 + + def test_size_takes_precedence(self): + """Test that explicit size takes precedence over linewidth.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + # If both are provided, size should win (it's more explicit) + plot = ggplot(df, aes(x="x", y="y")) + geom_line(size=3, linewidth=5) + fig = plot.draw() + # size=3 should be used since it was explicitly provided + assert fig.data[0].line.width == 3 + + +class TestGeomTextParse: + """Tests for geom_text parse parameter.""" + + def test_parse_default_false(self): + """Test that parse defaults to False.""" + df = pd.DataFrame({"x": [1], "y": [1], "label": ["alpha"]}) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_text() + fig = plot.draw() + # Without parse, text should be plain + assert fig.data[0].text[0] == "alpha" + + def test_parse_true_wraps_in_mathjax(self): + """Test that parse=True wraps text in $ for MathJax.""" + df = pd.DataFrame({"x": [1], "y": [1], "label": ["alpha"]}) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_text(parse=True) + fig = plot.draw() + # With parse, text should be wrapped in $...$ + assert fig.data[0].text[0] == "$alpha$" + + def test_parse_already_mathjax(self): + """Test that parse doesn't double-wrap already MathJax text.""" + df = pd.DataFrame({"x": [1], "y": [1], "label": ["$beta$"]}) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_text(parse=True) + fig = plot.draw() + # Should not become $$beta$$ + assert fig.data[0].text[0] == "$beta$" + + +class TestPositionExports: + """Tests that new position functions are properly exported.""" + + def test_position_fill_exported(self): + """Test that position_fill is exported and callable.""" + pos = position_fill() + assert pos is not None + + def test_position_nudge_exported(self): + """Test that position_nudge is exported and callable.""" + pos = position_nudge(x=0.1, y=0.1) + assert pos is not None + + def test_position_identity_exported(self): + """Test that position_identity is exported and callable.""" + pos = position_identity() + assert pos is not None + + def test_position_dodge2_exported(self): + """Test that position_dodge2 is exported and callable.""" + pos = position_dodge2() + assert pos is not None + + +# ============================================================================= +# EDGE CASE TESTS +# ============================================================================= + + +class TestStrokeEdgeCases: + """Edge case tests for geom_point stroke parameter.""" + + def test_stroke_with_large_value(self): + """Test stroke with large value.""" + df = pd.DataFrame({"x": [1, 2], "y": [1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(stroke=10) + fig = plot.draw() + assert fig.data[0].marker.line.width == 10 + + def test_stroke_with_float_value(self): + """Test stroke with float value.""" + df = pd.DataFrame({"x": [1, 2], "y": [1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(stroke=1.5) + fig = plot.draw() + assert fig.data[0].marker.line.width == 1.5 + + def test_stroke_with_color_aesthetic(self): + """Test stroke works with color aesthetic.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3], "cat": ["A", "A", "B"]}) + plot = ggplot(df, aes(x="x", y="y", color="cat")) + geom_point(stroke=2) + fig = plot.draw() + # Should have multiple traces, each with stroke + assert len(fig.data) >= 2 + for trace in fig.data: + assert trace.marker.line.width == 2 + + def test_stroke_with_shape_aesthetic(self): + """Test stroke works with shape aesthetic.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3], "cat": ["A", "A", "B"]}) + plot = ggplot(df, aes(x="x", y="y", shape="cat")) + geom_point(stroke=3) + fig = plot.draw() + assert fig is not None + + def test_stroke_with_size_parameter(self): + """Test stroke and size work together.""" + df = pd.DataFrame({"x": [1, 2], "y": [1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(size=15, stroke=3) + fig = plot.draw() + assert fig.data[0].marker.size == 15 + assert fig.data[0].marker.line.width == 3 + + def test_stroke_empty_dataframe(self): + """Test stroke with empty dataframe doesn't crash.""" + df = pd.DataFrame({"x": [], "y": []}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(stroke=2) + fig = plot.draw() + assert fig is not None + + +class TestArrowEdgeCases: + """Edge case tests for geom_segment arrow parameter.""" + + def test_arrow_multiple_segments(self): + """Test arrow with multiple segments.""" + df = pd.DataFrame({ + "x": [0, 1, 2], + "y": [0, 0, 0], + "xend": [1, 2, 3], + "yend": [1, 1, 1], + }) + plot = ggplot(df, aes(x="x", y="y", xend="xend", yend="yend")) + geom_segment( + arrow=True + ) + fig = plot.draw() + # Each segment should have arrow + assert len(fig.data) == 3 + for trace in fig.data: + assert trace.mode == "lines+markers" + + def test_arrow_with_color_grouping(self): + """Test arrow with color aesthetic.""" + df = pd.DataFrame({ + "x": [0, 1], + "y": [0, 0], + "xend": [1, 2], + "yend": [1, 1], + "cat": ["A", "B"], + }) + plot = ggplot(df, aes(x="x", y="y", xend="xend", yend="yend", color="cat")) + geom_segment( + arrow=True + ) + fig = plot.draw() + # Each category should have arrow + for trace in fig.data: + assert trace.mode == "lines+markers" + + def test_arrow_horizontal_segment(self): + """Test arrow on horizontal segment.""" + df = pd.DataFrame({"x": [0], "y": [1], "xend": [5], "yend": [1]}) + plot = ggplot(df, aes(x="x", y="y", xend="xend", yend="yend")) + geom_segment( + arrow=True + ) + fig = plot.draw() + assert fig.data[0].marker.angleref == "previous" + + def test_arrow_vertical_segment(self): + """Test arrow on vertical segment.""" + df = pd.DataFrame({"x": [1], "y": [0], "xend": [1], "yend": [5]}) + plot = ggplot(df, aes(x="x", y="y", xend="xend", yend="yend")) + geom_segment( + arrow=True + ) + fig = plot.draw() + assert fig.data[0].marker.angleref == "previous" + + def test_arrow_size_zero(self): + """Test arrow with size 0 (effectively no arrow head).""" + df = pd.DataFrame({"x": [0], "y": [0], "xend": [1], "yend": [1]}) + plot = ggplot(df, aes(x="x", y="y", xend="xend", yend="yend")) + geom_segment( + arrow=True, arrow_size=0 + ) + fig = plot.draw() + assert fig.data[0].marker.size[1] == 0 + + +class TestErrorbarWidthEdgeCases: + """Edge case tests for geom_errorbar width parameter.""" + + def test_width_zero(self): + """Test width=0 (no caps).""" + df = pd.DataFrame({"x": [1], "y": [5], "ymin": [4], "ymax": [6]}) + plot = ggplot(df, aes(x="x", y="y", ymin="ymin", ymax="ymax")) + geom_errorbar( + width=0 + ) + fig = plot.draw() + assert fig.data[0].error_y.width == 0 + + def test_width_with_yerr_aesthetic(self): + """Test width with yerr aesthetic instead of ymin/ymax.""" + df = pd.DataFrame({"x": [1, 2], "y": [5, 6], "err": [0.5, 0.3]}) + plot = ggplot(df, aes(x="x", y="y", yerr="err")) + geom_errorbar(width=8) + fig = plot.draw() + assert fig.data[0].error_y.width == 8 + + def test_width_with_grouped_data(self): + """Test width with grouped error bars.""" + df = pd.DataFrame({ + "x": [1, 1, 2, 2], + "y": [5, 6, 7, 8], + "ymin": [4, 5, 6, 7], + "ymax": [6, 7, 8, 9], + "group": ["A", "B", "A", "B"], + }) + plot = ggplot(df, aes(x="x", y="y", ymin="ymin", ymax="ymax", color="group")) + geom_errorbar( + width=6 + ) + fig = plot.draw() + for trace in fig.data: + assert trace.error_y.width == 6 + + +class TestLinewidthEdgeCases: + """Edge case tests for linewidth alias.""" + + def test_linewidth_in_geom_smooth(self): + """Test linewidth alias works in geom_smooth.""" + df = pd.DataFrame({"x": [1, 2, 3, 4, 5], "y": [1, 2, 1.5, 3, 2.5]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_smooth(linewidth=4, se=False) + fig = plot.draw() + # Find the line trace (not the CI ribbon) + line_traces = [t for t in fig.data if t.mode == "lines"] + assert len(line_traces) > 0 + assert line_traces[0].line.width == 4 + + def test_linewidth_in_geom_density(self): + """Test linewidth alias works in geom_density.""" + np.random.seed(42) + df = pd.DataFrame({"x": np.random.normal(0, 1, 100)}) + plot = ggplot(df, aes(x="x")) + geom_density(linewidth=3) + fig = plot.draw() + assert fig is not None + + def test_linewidth_with_color_aesthetic(self): + """Test linewidth with color grouping.""" + df = pd.DataFrame({ + "x": [1, 2, 3, 1, 2, 3], + "y": [1, 2, 3, 2, 3, 4], + "group": ["A", "A", "A", "B", "B", "B"], + }) + plot = ggplot(df, aes(x="x", y="y", color="group")) + geom_line(linewidth=4) + fig = plot.draw() + for trace in fig.data: + assert trace.line.width == 4 + + +class TestParseEdgeCases: + """Edge case tests for geom_text parse parameter.""" + + def test_parse_complex_latex(self): + """Test parse with complex LaTeX expressions.""" + df = pd.DataFrame({ + "x": [1, 2, 3], + "y": [1, 2, 3], + "label": ["x^2", "\\frac{1}{2}", "\\sqrt{x}"], + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_text(parse=True) + fig = plot.draw() + assert fig.data[0].text[0] == "$x^2$" + assert fig.data[0].text[1] == "$\\frac{1}{2}$" + assert fig.data[0].text[2] == "$\\sqrt{x}$" + + def test_parse_mixed_content(self): + """Test parse with mix of regular and special text.""" + df = pd.DataFrame({ + "x": [1, 2], + "y": [1, 2], + "label": ["regular", "\\alpha"], + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_text(parse=True) + fig = plot.draw() + assert fig.data[0].text[0] == "$regular$" + assert fig.data[0].text[1] == "$\\alpha$" + + def test_parse_with_color_aesthetic(self): + """Test parse with color grouping.""" + df = pd.DataFrame({ + "x": [1, 2], + "y": [1, 2], + "label": ["x", "y"], + "cat": ["A", "B"], + }) + plot = ggplot(df, aes(x="x", y="y", label="label", color="cat")) + geom_text( + parse=True + ) + fig = plot.draw() + # All traces should have parsed text + for trace in fig.data: + for text in trace.text: + assert text.startswith("$") and text.endswith("$") + + def test_parse_empty_string(self): + """Test parse with empty string label.""" + df = pd.DataFrame({"x": [1], "y": [1], "label": [""]}) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_text(parse=True) + fig = plot.draw() + assert fig.data[0].text[0] == "$$" # Empty MathJax + + def test_parse_numeric_labels(self): + """Test parse with numeric labels.""" + df = pd.DataFrame({"x": [1, 2], "y": [1, 2], "label": [123, 456]}) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_text(parse=True) + fig = plot.draw() + assert fig.data[0].text[0] == "$123$" + assert fig.data[0].text[1] == "$456$" + + +# ============================================================================= +# INTEGRATION TESTS +# ============================================================================= + + +class TestStrokeWithFaceting: + """Integration tests for stroke with faceting.""" + + def test_stroke_with_facet_wrap(self): + """Test stroke parameter works with facet_wrap.""" + df = pd.DataFrame({ + "x": [1, 2, 3, 1, 2, 3], + "y": [1, 2, 3, 2, 3, 4], + "panel": ["A", "A", "A", "B", "B", "B"], + }) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point(stroke=2) + + facet_wrap("panel") + ) + fig = plot.draw() + # Both panels should have points with stroke + for trace in fig.data: + if trace.mode == "markers": + assert trace.marker.line.width == 2 + + def test_stroke_with_facet_grid(self): + """Test stroke parameter works with facet_grid.""" + df = pd.DataFrame({ + "x": [1, 2, 1, 2], + "y": [1, 2, 2, 3], + "row_var": ["R1", "R1", "R2", "R2"], + "col_var": ["C1", "C2", "C1", "C2"], + }) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point(stroke=3) + + facet_grid(rows="row_var", cols="col_var") + ) + fig = plot.draw() + assert fig is not None + + def test_stroke_with_color_and_facet(self): + """Test stroke with both color aesthetic and faceting.""" + df = pd.DataFrame({ + "x": [1, 2, 3, 4], + "y": [1, 2, 3, 4], + "color_var": ["A", "B", "A", "B"], + "facet_var": ["P1", "P1", "P2", "P2"], + }) + plot = ( + ggplot(df, aes(x="x", y="y", color="color_var")) + + geom_point(stroke=2) + + facet_wrap("facet_var") + ) + fig = plot.draw() + # All point traces should have stroke + for trace in fig.data: + if hasattr(trace, 'marker') and trace.mode == "markers": + assert trace.marker.line.width == 2 + + +class TestArrowIntegration: + """Integration tests for arrow with other features.""" + + def test_arrow_with_facet_wrap(self): + """Test arrow works with facet_wrap.""" + df = pd.DataFrame({ + "x": [0, 0], + "y": [0, 0], + "xend": [1, 1], + "yend": [1, 1], + "panel": ["A", "B"], + }) + plot = ( + ggplot(df, aes(x="x", y="y", xend="xend", yend="yend")) + + geom_segment(arrow=True) + + facet_wrap("panel") + ) + fig = plot.draw() + for trace in fig.data: + assert trace.mode == "lines+markers" + + def test_arrow_with_multiple_geoms(self): + """Test arrow segment combined with points.""" + df_segments = pd.DataFrame({ + "x": [0], + "y": [0], + "xend": [1], + "yend": [1], + }) + df_points = pd.DataFrame({"x": [0, 1], "y": [0, 1]}) + plot = ( + ggplot(df_points, aes(x="x", y="y")) + + geom_point(size=10) + + geom_segment( + data=df_segments, + mapping=aes(x="x", y="y", xend="xend", yend="yend"), + arrow=True, + ) + ) + fig = plot.draw() + # Should have both points and arrow segment + modes = [t.mode for t in fig.data] + assert "markers" in modes + assert "lines+markers" in modes + + +class TestLinewidthIntegration: + """Integration tests for linewidth alias.""" + + def test_linewidth_with_multiple_lines(self): + """Test linewidth with multiple line groups.""" + df = pd.DataFrame({ + "x": [1, 2, 3] * 3, + "y": [1, 2, 3, 2, 3, 4, 3, 4, 5], + "group": ["A"] * 3 + ["B"] * 3 + ["C"] * 3, + }) + plot = ggplot(df, aes(x="x", y="y", color="group")) + geom_line(linewidth=3) + fig = plot.draw() + assert len(fig.data) == 3 + for trace in fig.data: + assert trace.line.width == 3 + + def test_linewidth_with_ribbon(self): + """Test linewidth alias doesn't break geom_ribbon.""" + df = pd.DataFrame({ + "x": [1, 2, 3], + "y": [2, 3, 2.5], + "ymin": [1, 2, 1.5], + "ymax": [3, 4, 3.5], + }) + plot = ggplot(df, aes(x="x", y="y", ymin="ymin", ymax="ymax")) + geom_ribbon( + linewidth=2 + ) + fig = plot.draw() + assert fig is not None + + +class TestPositionIntegration: + """Integration tests for new position functions.""" + + def test_position_fill_with_bar(self): + """Test position_fill with bar chart (100% stacked).""" + df = pd.DataFrame({ + "category": ["A", "A", "B", "B"], + "group": ["X", "Y", "X", "Y"], + "value": [10, 20, 30, 40], + }) + plot = ( + ggplot(df, aes(x="category", y="value", fill="group")) + + geom_bar(stat="identity", position=position_fill()) + ) + fig = plot.draw() + assert fig is not None + + def test_position_nudge_with_text(self): + """Test position_nudge with text labels.""" + df = pd.DataFrame({ + "x": [1, 2, 3], + "y": [1, 2, 3], + "label": ["A", "B", "C"], + }) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + geom_text(aes(label="label"), nudge_y=0.2) + ) + fig = plot.draw() + assert fig is not None + + def test_position_dodge2_with_boxplot(self): + """Test position_dodge2 is callable for boxplots.""" + pos = position_dodge2(padding=0.2) + assert pos.padding == 0.2 + + def test_position_identity_preserves_data(self): + """Test position_identity doesn't modify positions.""" + pos = position_identity() + assert pos is not None + + +class TestMultipleNewParameters: + """Integration tests combining multiple new parameters.""" + + def test_stroke_and_linewidth_together(self): + """Test using stroke (point) and linewidth (line) in same plot.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_line(linewidth=3) + + geom_point(stroke=2, size=10) + ) + fig = plot.draw() + # Find line and point traces + line_trace = [t for t in fig.data if t.mode == "lines"][0] + point_trace = [t for t in fig.data if t.mode == "markers"][0] + assert line_trace.line.width == 3 + assert point_trace.marker.line.width == 2 + + def test_all_new_params_complex_plot(self): + """Test complex plot with multiple new parameters.""" + np.random.seed(42) + df = pd.DataFrame({ + "x": [1, 2, 3, 4, 5], + "y": [2, 3, 2.5, 4, 3.5], + "ymin": [1.5, 2.5, 2, 3.5, 3], + "ymax": [2.5, 3.5, 3, 4.5, 4], + "label": ["A", "B", "C", "D", "E"], + }) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_line(linewidth=2) + + geom_point(stroke=1.5, size=8) + + geom_errorbar(aes(ymin="ymin", ymax="ymax"), width=6) + + geom_text(aes(label="label"), parse=True, nudge_y=0.3) + + theme_minimal() + + labs(title="Combined New Parameters") + ) + fig = plot.draw() + assert fig is not None + assert len(fig.data) >= 3 # At least line, points, errorbars + + +# ============================================================================= +# VISUAL REGRESSION TESTS +# ============================================================================= + + +class TestVisualRegression: + """ + Visual regression tests using figure JSON comparison. + + These tests capture the essential visual properties of figures + and compare them against expected values to detect regressions. + """ + + @staticmethod + def get_figure_signature(fig): + """ + Generate a signature of key visual properties from a figure. + + Returns a dict with essential properties that define the visual output. + """ + signature = { + "num_traces": len(fig.data), + "traces": [], + } + + for i, trace in enumerate(fig.data): + trace_sig = { + "type": trace.type, + "mode": getattr(trace, "mode", None), + } + + # Capture marker properties + if hasattr(trace, "marker") and trace.marker: + marker = trace.marker + symbol = getattr(marker, "symbol", None) + # Convert tuple to list for consistent comparison + if isinstance(symbol, tuple): + symbol = list(symbol) + size = getattr(marker, "size", None) + if isinstance(size, tuple): + size = list(size) + trace_sig["marker"] = { + "size": size, + "symbol": symbol, + } + if hasattr(marker, "line") and marker.line: + trace_sig["marker"]["line_width"] = getattr(marker.line, "width", None) + + # Capture line properties + if hasattr(trace, "line") and trace.line: + trace_sig["line"] = { + "width": getattr(trace.line, "width", None), + "dash": getattr(trace.line, "dash", None), + } + + # Capture error bar properties + if hasattr(trace, "error_y") and trace.error_y: + trace_sig["error_y"] = { + "width": getattr(trace.error_y, "width", None), + } + + # Capture text + if hasattr(trace, "text") and trace.text is not None: + text = trace.text + # Handle numpy arrays and pandas Series + if hasattr(text, "tolist"): + text = text.tolist() + if isinstance(text, (list, tuple)): + trace_sig["text_sample"] = list(text)[:3] + else: + trace_sig["text_sample"] = [text] + + signature["traces"].append(trace_sig) + + return signature + + def test_stroke_visual_signature(self): + """Test that stroke produces expected visual signature.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point(stroke=2.5) + fig = plot.draw() + + sig = self.get_figure_signature(fig) + + assert sig["num_traces"] == 1 + assert sig["traces"][0]["type"] == "scatter" + assert sig["traces"][0]["mode"] == "markers" + assert sig["traces"][0]["marker"]["line_width"] == 2.5 + + def test_arrow_visual_signature(self): + """Test that arrow produces expected visual signature.""" + df = pd.DataFrame({"x": [0], "y": [0], "xend": [1], "yend": [1]}) + plot = ggplot(df, aes(x="x", y="y", xend="xend", yend="yend")) + geom_segment( + arrow=True, arrow_size=18 + ) + fig = plot.draw() + + sig = self.get_figure_signature(fig) + + assert sig["num_traces"] == 1 + assert sig["traces"][0]["mode"] == "lines+markers" + # Arrow marker at end + assert sig["traces"][0]["marker"]["symbol"] == ["circle", "arrow"] + assert sig["traces"][0]["marker"]["size"] == [0, 18] + + def test_errorbar_width_visual_signature(self): + """Test that errorbar width produces expected visual signature.""" + df = pd.DataFrame({"x": [1, 2], "y": [5, 6], "ymin": [4, 5], "ymax": [6, 7]}) + plot = ggplot(df, aes(x="x", y="y", ymin="ymin", ymax="ymax")) + geom_errorbar( + width=12 + ) + fig = plot.draw() + + sig = self.get_figure_signature(fig) + + assert sig["traces"][0]["error_y"]["width"] == 12 + + def test_linewidth_visual_signature(self): + """Test that linewidth produces expected visual signature.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_line(linewidth=4.5) + fig = plot.draw() + + sig = self.get_figure_signature(fig) + + assert sig["traces"][0]["line"]["width"] == 4.5 + + def test_parse_visual_signature(self): + """Test that parse produces expected visual signature.""" + df = pd.DataFrame({ + "x": [1, 2], + "y": [1, 2], + "label": ["\\alpha", "\\beta"], + }) + plot = ggplot(df, aes(x="x", y="y", label="label")) + geom_text(parse=True) + fig = plot.draw() + + sig = self.get_figure_signature(fig) + + # Text should be wrapped in $...$ + assert sig["traces"][0]["text_sample"] == ["$\\alpha$", "$\\beta$"] + + def test_combined_visual_signature(self): + """Test combined parameters produce expected visual signature.""" + df = pd.DataFrame({ + "x": [1, 2, 3], + "y": [1, 2, 3], + "label": ["a", "b", "c"], + }) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_line(linewidth=2) + + geom_point(stroke=1, size=10) + + geom_text(aes(label="label"), parse=True, nudge_y=0.1) + ) + fig = plot.draw() + + sig = self.get_figure_signature(fig) + + # Should have 3 traces: line, points, text + assert sig["num_traces"] == 3 + + # Find each trace type + line_trace = next(t for t in sig["traces"] if t.get("mode") == "lines") + point_trace = next(t for t in sig["traces"] if t.get("mode") == "markers") + text_trace = next(t for t in sig["traces"] if t.get("mode") == "text") + + assert line_trace["line"]["width"] == 2 + assert point_trace["marker"]["size"] == 10 + assert point_trace["marker"]["line_width"] == 1 + assert text_trace["text_sample"] == ["$a$", "$b$", "$c$"] + + +class TestVisualRegressionWithColors: + """Visual regression tests with color aesthetics.""" + + def test_stroke_with_colors_signature(self): + """Test stroke with color aesthetic produces correct traces.""" + df = pd.DataFrame({ + "x": [1, 2, 3, 4], + "y": [1, 2, 3, 4], + "cat": ["A", "A", "B", "B"], + }) + plot = ggplot(df, aes(x="x", y="y", color="cat")) + geom_point(stroke=2) + fig = plot.draw() + + # Should have 2 traces (one per category) + assert len(fig.data) == 2 + + # Both should have stroke + for trace in fig.data: + assert trace.marker.line.width == 2 + # Each trace should have different colors + assert trace.marker.color is not None + + def test_linewidth_with_colors_signature(self): + """Test linewidth with color aesthetic produces correct traces.""" + df = pd.DataFrame({ + "x": [1, 2, 3] * 2, + "y": [1, 2, 3, 2, 3, 4], + "cat": ["A"] * 3 + ["B"] * 3, + }) + plot = ggplot(df, aes(x="x", y="y", color="cat")) + geom_line(linewidth=3) + fig = plot.draw() + + # Should have 2 traces + assert len(fig.data) == 2 + + # Both should have linewidth + for trace in fig.data: + assert trace.line.width == 3 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/pytest/test_parameter_audit.py b/pytest/test_parameter_audit.py new file mode 100644 index 0000000..d954248 --- /dev/null +++ b/pytest/test_parameter_audit.py @@ -0,0 +1,387 @@ +# pytest/test_parameter_audit.py +""" +Tests for parameter audit features added for v1.0.0. + +Tests cover: +- Base class: na_rm, show_legend, colour alias +- geom_col: width parameter +- geom_smooth: fullrange parameter +- geom_ribbon: linetype, size parameters +- geom_area: position parameter +- All geoms: linewidth alias for size +""" + +import numpy as np +import pandas as pd +import pytest + +from ggplotly import ( + ggplot, + aes, + geom_point, + geom_line, + geom_col, + geom_bar, + geom_smooth, + geom_ribbon, + geom_area, + geom_path, + geom_step, + geom_segment, + geom_density, + labs, +) + + +# ============================================================================= +# Test Fixtures +# ============================================================================= + +@pytest.fixture +def basic_df(): + """Basic DataFrame for testing.""" + return pd.DataFrame({ + 'x': [1, 2, 3, 4, 5], + 'y': [2, 4, 3, 5, 4], + }) + + +@pytest.fixture +def df_with_na(): + """DataFrame with missing values.""" + return pd.DataFrame({ + 'x': [1, 2, np.nan, 4, 5], + 'y': [2, np.nan, 3, 5, 4], + }) + + +@pytest.fixture +def category_df(): + """DataFrame with categories.""" + return pd.DataFrame({ + 'category': ['A', 'B', 'C', 'A', 'B', 'C'], + 'value': [10, 15, 12, 8, 18, 14], + }) + + +@pytest.fixture +def ribbon_df(): + """DataFrame for ribbon plots.""" + return pd.DataFrame({ + 'x': [1, 2, 3, 4, 5], + 'y': [3, 4, 3.5, 5, 4.5], + 'ymin': [2, 3, 2.5, 4, 3.5], + 'ymax': [4, 5, 4.5, 6, 5.5], + }) + + +@pytest.fixture +def area_df(): + """DataFrame for stacked area plots.""" + return pd.DataFrame({ + 'x': [1, 2, 3, 1, 2, 3], + 'y': [2, 3, 2.5, 1, 2, 1.5], + 'group': ['A', 'A', 'A', 'B', 'B', 'B'], + }) + + +@pytest.fixture +def segment_df(): + """DataFrame for segment plots.""" + return pd.DataFrame({ + 'x': [1, 2], + 'y': [1, 2], + 'xend': [3, 4], + 'yend': [2, 3], + }) + + +# ============================================================================= +# Priority 1: Base Class Parameters +# ============================================================================= + +class TestNaRmParameter: + """Tests for na_rm parameter in base Geom class.""" + + def test_na_rm_default_false(self, df_with_na): + """na_rm should default to False.""" + g = geom_point() + assert g.params.get('na_rm') == False + + def test_na_rm_true_removes_missing(self, df_with_na): + """na_rm=True should remove rows with NA in mapped columns.""" + p = ggplot(df_with_na, aes(x='x', y='y')) + geom_point(na_rm=True) + fig = p.draw() + # Should have 3 points (rows 0, 3, 4 have complete data) + assert len(fig.data) >= 1 + + def test_na_rm_false_keeps_data(self, df_with_na): + """na_rm=False should keep all data (default behavior).""" + p = ggplot(df_with_na, aes(x='x', y='y')) + geom_point(na_rm=False) + fig = p.draw() + assert len(fig.data) >= 1 + + def test_na_rm_with_geom_line(self, df_with_na): + """na_rm should work with geom_line.""" + p = ggplot(df_with_na, aes(x='x', y='y')) + geom_line(na_rm=True) + fig = p.draw() + assert len(fig.data) >= 1 + + def test_na_rm_with_geom_bar(self): + """na_rm should work with geom_bar.""" + df = pd.DataFrame({ + 'category': ['A', 'B', np.nan, 'D'], + 'value': [10, np.nan, 12, 14], + }) + p = ggplot(df, aes(x='category', y='value')) + geom_bar(stat='identity', na_rm=True) + fig = p.draw() + assert len(fig.data) >= 1 + + +class TestShowLegendParameter: + """Tests for show_legend parameter in base Geom class.""" + + def test_show_legend_default_true(self): + """show_legend should default to True.""" + g = geom_point() + assert g.params.get('show_legend') == True + + def test_show_legend_false(self, basic_df): + """show_legend=False should be accepted.""" + p = ggplot(basic_df, aes(x='x', y='y')) + geom_point(show_legend=False) + fig = p.draw() + assert len(fig.data) >= 1 + + def test_showlegend_alias(self): + """showlegend should be an alias for show_legend.""" + g = geom_point(showlegend=False) + assert g.params.get('show_legend') == False + + +class TestColourAlias: + """Tests for colour/color alias in base Geom class.""" + + def test_colour_alias_works(self, basic_df): + """colour should work as alias for color.""" + p = ggplot(basic_df, aes(x='x', y='y')) + geom_point(colour='red') + fig = p.draw() + assert len(fig.data) >= 1 + # The color should be applied + assert 'red' in str(fig.data[0].marker.color).lower() or fig.data[0].marker.color == 'red' + + def test_color_takes_precedence(self, basic_df): + """If both color and colour specified, color should take precedence.""" + g = geom_point(color='blue', colour='red') + assert g.params.get('color') == 'blue' + + +# ============================================================================= +# Priority 2: Geom-Specific Parameters +# ============================================================================= + +class TestGeomColWidth: + """Tests for width parameter in geom_col.""" + + def test_width_default(self): + """geom_col should have default width of 0.9.""" + g = geom_col() + assert g.params.get('width') == 0.9 + + def test_width_custom(self, category_df): + """Custom width should be applied.""" + p = ggplot(category_df, aes(x='category', y='value')) + geom_col(width=0.5) + fig = p.draw() + assert len(fig.data) >= 1 + # Width should be set on the trace + assert fig.data[0].width == 0.5 + + def test_width_narrow(self, category_df): + """Narrow width (0.3) should work.""" + p = ggplot(category_df, aes(x='category', y='value')) + geom_col(width=0.3) + fig = p.draw() + assert fig.data[0].width == 0.3 + + def test_width_full(self, category_df): + """Full width (1.0) should work.""" + p = ggplot(category_df, aes(x='category', y='value')) + geom_col(width=1.0) + fig = p.draw() + assert fig.data[0].width == 1.0 + + +class TestGeomSmoothFullrange: + """Tests for fullrange parameter in geom_smooth.""" + + def test_fullrange_default_false(self): + """fullrange should default to False.""" + g = geom_smooth() + assert g.params.get('fullrange') == False + + def test_fullrange_true_accepted(self, basic_df): + """fullrange=True should be accepted.""" + p = ggplot(basic_df, aes(x='x', y='y')) + geom_smooth(fullrange=True, se=False) + fig = p.draw() + assert len(fig.data) >= 1 + + def test_n_parameter(self): + """n parameter should control number of prediction points.""" + g = geom_smooth(n=100) + assert g.params.get('n') == 100 + + +class TestGeomRibbonParams: + """Tests for linetype and size parameters in geom_ribbon.""" + + def test_ribbon_default_params(self): + """geom_ribbon should have default alpha and size.""" + g = geom_ribbon() + assert g.params.get('alpha') == 0.5 + assert g.params.get('size') == 1 + + def test_ribbon_custom_size(self, ribbon_df): + """Custom size should be passed through.""" + p = ggplot(ribbon_df, aes(x='x', ymin='ymin', ymax='ymax')) + geom_ribbon(size=3) + fig = p.draw() + assert len(fig.data) >= 1 + + def test_ribbon_linetype(self, ribbon_df): + """linetype should be passed through.""" + p = ggplot(ribbon_df, aes(x='x', ymin='ymin', ymax='ymax')) + geom_ribbon(linetype='dash') + fig = p.draw() + assert len(fig.data) >= 1 + + +class TestGeomAreaPosition: + """Tests for position parameter in geom_area.""" + + def test_position_default_identity(self): + """position should default to 'identity'.""" + g = geom_area() + assert g.params.get('position') == 'identity' + + def test_position_stack(self, area_df): + """position='stack' should create stacked areas.""" + p = ggplot(area_df, aes(x='x', y='y', fill='group')) + geom_area(position='stack') + fig = p.draw() + assert len(fig.data) >= 1 + # Stack mode should set stackgroup + assert any(hasattr(trace, 'stackgroup') and trace.stackgroup for trace in fig.data) + + def test_position_identity(self, area_df): + """position='identity' should not stack.""" + p = ggplot(area_df, aes(x='x', y='y', fill='group')) + geom_area(position='identity') + fig = p.draw() + assert len(fig.data) >= 1 + + +# ============================================================================= +# Priority 3: Linewidth Alias Consistency +# ============================================================================= + +class TestLinewidthAlias: + """Tests for linewidth alias across all line-based geoms.""" + + def test_geom_line_linewidth(self, basic_df): + """geom_line should accept linewidth.""" + p = ggplot(basic_df, aes(x='x', y='y')) + geom_line(linewidth=4) + fig = p.draw() + assert fig.data[0].line.width == 4 + + def test_geom_path_linewidth(self, basic_df): + """geom_path should accept linewidth.""" + p = ggplot(basic_df, aes(x='x', y='y')) + geom_path(linewidth=3) + fig = p.draw() + assert fig.data[0].line.width == 3 + + def test_geom_step_linewidth(self, basic_df): + """geom_step should accept linewidth.""" + p = ggplot(basic_df, aes(x='x', y='y')) + geom_step(linewidth=2) + fig = p.draw() + assert fig.data[0].line.width == 2 + + def test_geom_segment_linewidth(self, segment_df): + """geom_segment should accept linewidth.""" + p = ggplot(segment_df, aes(x='x', y='y', xend='xend', yend='yend')) + geom_segment(linewidth=2) + fig = p.draw() + assert len(fig.data) >= 1 + + def test_geom_smooth_linewidth(self, basic_df): + """geom_smooth should accept linewidth.""" + p = ggplot(basic_df, aes(x='x', y='y')) + geom_smooth(linewidth=5, se=False) + fig = p.draw() + assert fig.data[0].line.width == 5 + + def test_linewidth_size_both_specified(self, basic_df): + """If both linewidth and size specified, size should take precedence.""" + g = geom_line(linewidth=10, size=5) + # size was explicitly provided, so it should win + assert g.params.get('size') == 5 + + +# ============================================================================= +# Integration Tests +# ============================================================================= + +class TestParameterCombinations: + """Integration tests for combining multiple new parameters.""" + + def test_na_rm_with_colour(self, df_with_na): + """na_rm and colour should work together.""" + p = ggplot(df_with_na, aes(x='x', y='y')) + geom_point(na_rm=True, colour='purple') + fig = p.draw() + assert len(fig.data) >= 1 + + def test_linewidth_with_linetype(self, basic_df): + """linewidth and linetype should work together.""" + p = ggplot(basic_df, aes(x='x', y='y')) + geom_line(linewidth=3, linetype='dash') + fig = p.draw() + assert fig.data[0].line.width == 3 + assert fig.data[0].line.dash == 'dash' + + def test_area_position_with_alpha(self, area_df): + """position='stack' should work with alpha.""" + p = ggplot(area_df, aes(x='x', y='y', fill='group')) + geom_area(position='stack', alpha=0.7) + fig = p.draw() + assert len(fig.data) >= 1 + + def test_col_width_with_fill(self, category_df): + """width should work with fill aesthetic.""" + p = ggplot(category_df, aes(x='category', y='value')) + geom_col(width=0.6, fill='coral') + fig = p.draw() + assert fig.data[0].width == 0.6 + + +# ============================================================================= +# Visual Regression Tests +# ============================================================================= + +class TestParameterAuditVisualSignatures: + """Visual regression tests verifying expected plot structure.""" + + def test_geom_col_width_signature(self, category_df): + """Verify geom_col width creates expected structure.""" + p = ggplot(category_df, aes(x='category', y='value')) + geom_col(width=0.7) + fig = p.draw() + + assert fig.data[0].type == 'bar' + assert fig.data[0].width == 0.7 + + def test_geom_area_stack_signature(self, area_df): + """Verify stacked area creates expected structure.""" + p = ggplot(area_df, aes(x='x', y='y', fill='group')) + geom_area(position='stack') + fig = p.draw() + + # Should have scatter traces with stackgroup + scatter_traces = [t for t in fig.data if t.type == 'scatter'] + assert len(scatter_traces) >= 1 + + def test_base_params_inherited(self, basic_df): + """Verify base class params are inherited by all geoms.""" + geoms = [ + geom_point(na_rm=True, show_legend=False), + geom_line(na_rm=True, show_legend=False), + geom_bar(na_rm=True, show_legend=False), + ] + + for g in geoms: + assert g.params.get('na_rm') == True + assert g.params.get('show_legend') == False diff --git a/pytest/test_scale_reverse_coord_fixed.py b/pytest/test_scale_reverse_coord_fixed.py new file mode 100644 index 0000000..236bd5f --- /dev/null +++ b/pytest/test_scale_reverse_coord_fixed.py @@ -0,0 +1,446 @@ +""" +Tests for scale_x_reverse, scale_y_reverse, and coord_fixed. + +Covers all four test categories: +1. Basic functionality tests +2. Edge case tests +3. Integration tests (with other scales/coords, faceting) +4. Visual regression tests +""" + +import numpy as np +import pandas as pd +import pytest + +from ggplotly import ( + aes, + coord_fixed, + facet_wrap, + geom_line, + geom_path, + geom_point, + ggplot, + scale_x_reverse, + scale_y_reverse, +) + + +# ============================================================================= +# SCALE_X_REVERSE TESTS +# ============================================================================= + + +class TestScaleXReverseBasic: + """Basic functionality tests for scale_x_reverse.""" + + def test_scale_x_reverse_basic(self): + """Test basic x-axis reversal.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + scale_x_reverse() + fig = plot.draw() + # Check that autorange is set to reversed + assert fig.layout.xaxis.autorange == "reversed" + + def test_scale_x_reverse_with_name(self): + """Test reversed x-axis with custom name.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + scale_x_reverse(name="Reversed X") + fig = plot.draw() + assert fig.layout.xaxis.title.text == "Reversed X" + + def test_scale_x_reverse_with_limits(self): + """Test reversed x-axis with explicit limits.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + scale_x_reverse(limits=(0, 10)) + fig = plot.draw() + # Limits should be reversed (high to low) + xrange = fig.layout.xaxis.range + assert xrange[0] > xrange[1] # First value greater than second (reversed) + + def test_scale_x_reverse_with_breaks(self): + """Test reversed x-axis with custom breaks.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + scale_x_reverse(breaks=[1, 2, 3]) + ) + fig = plot.draw() + assert fig.layout.xaxis.tickmode == "array" + assert list(fig.layout.xaxis.tickvals) == [1, 2, 3] + + def test_scale_x_reverse_with_labels(self): + """Test reversed x-axis with custom labels.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + scale_x_reverse(breaks=[1, 2, 3], labels=["A", "B", "C"]) + ) + fig = plot.draw() + assert list(fig.layout.xaxis.ticktext) == ["A", "B", "C"] + + +class TestScaleYReverseBasic: + """Basic functionality tests for scale_y_reverse.""" + + def test_scale_y_reverse_basic(self): + """Test basic y-axis reversal.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + scale_y_reverse() + fig = plot.draw() + assert fig.layout.yaxis.autorange == "reversed" + + def test_scale_y_reverse_with_name(self): + """Test reversed y-axis with custom name.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + scale_y_reverse(name="Depth") + fig = plot.draw() + assert fig.layout.yaxis.title.text == "Depth" + + def test_scale_y_reverse_with_limits(self): + """Test reversed y-axis with explicit limits.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + scale_y_reverse(limits=(0, 100)) + fig = plot.draw() + # Limits should be reversed + yrange = fig.layout.yaxis.range + assert yrange[0] > yrange[1] + + +# ============================================================================= +# COORD_FIXED TESTS +# ============================================================================= + + +class TestCoordFixedBasic: + """Basic functionality tests for coord_fixed.""" + + def test_coord_fixed_basic(self): + """Test basic fixed aspect ratio (1:1).""" + df = pd.DataFrame({"x": [0, 1, 2], "y": [0, 1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + coord_fixed() + fig = plot.draw() + # Y-axis should be anchored to x-axis with ratio 1 + assert fig.layout.yaxis.scaleanchor == "x" + assert fig.layout.yaxis.scaleratio == 1 + + def test_coord_fixed_custom_ratio(self): + """Test fixed aspect ratio with custom ratio.""" + df = pd.DataFrame({"x": [0, 1, 2], "y": [0, 1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + coord_fixed(ratio=2) + fig = plot.draw() + assert fig.layout.yaxis.scaleratio == 2 + + def test_coord_fixed_ratio_half(self): + """Test fixed aspect ratio with ratio < 1.""" + df = pd.DataFrame({"x": [0, 1, 2], "y": [0, 1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + coord_fixed(ratio=0.5) + fig = plot.draw() + assert fig.layout.yaxis.scaleratio == 0.5 + + def test_coord_fixed_with_xlim(self): + """Test fixed aspect ratio with x-axis limits.""" + df = pd.DataFrame({"x": [0, 1, 2], "y": [0, 1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + coord_fixed(xlim=(0, 5)) + fig = plot.draw() + assert fig.layout.xaxis.range is not None + + def test_coord_fixed_with_ylim(self): + """Test fixed aspect ratio with y-axis limits.""" + df = pd.DataFrame({"x": [0, 1, 2], "y": [0, 1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + coord_fixed(ylim=(0, 5)) + fig = plot.draw() + assert fig.layout.yaxis.range is not None + + def test_coord_fixed_aspect_ratio(self): + """Test that coord_fixed properly sets the aspect ratio via scaleanchor/scaleratio.""" + df = pd.DataFrame({"x": [0, 1, 2], "y": [0, 1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + coord_fixed() + fig = plot.draw() + # coord_fixed uses scaleanchor/scaleratio for aspect ratio, not constrain + assert fig.layout.yaxis.scaleanchor == "x" + assert fig.layout.yaxis.scaleratio == 1 + + +# ============================================================================= +# EDGE CASE TESTS +# ============================================================================= + + +class TestReverseScaleEdgeCases: + """Edge case tests for reverse scales.""" + + def test_scale_x_reverse_empty_data(self): + """Test reverse scale with empty data.""" + df = pd.DataFrame({"x": [], "y": []}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + scale_x_reverse() + fig = plot.draw() # Should not crash + assert fig.layout.xaxis.autorange == "reversed" + + def test_scale_y_reverse_single_point(self): + """Test reverse scale with single data point.""" + df = pd.DataFrame({"x": [5], "y": [5]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + scale_y_reverse() + fig = plot.draw() + assert fig.layout.yaxis.autorange == "reversed" + + def test_scale_x_reverse_negative_values(self): + """Test reverse scale with negative values.""" + df = pd.DataFrame({"x": [-10, -5, 0, 5], "y": [1, 2, 3, 4]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + scale_x_reverse() + fig = plot.draw() + assert fig.layout.xaxis.autorange == "reversed" + + def test_scale_reverse_callable_labels(self): + """Test reverse scale with callable labels.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + scale_x_reverse(breaks=[1, 2, 3], labels=lambda x: [f"Val {v}" for v in x]) + ) + fig = plot.draw() + assert list(fig.layout.xaxis.ticktext) == ["Val 1", "Val 2", "Val 3"] + + def test_both_axes_reversed(self): + """Test both x and y axes reversed.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + scale_x_reverse() + + scale_y_reverse() + ) + fig = plot.draw() + assert fig.layout.xaxis.autorange == "reversed" + assert fig.layout.yaxis.autorange == "reversed" + + +class TestCoordFixedEdgeCases: + """Edge case tests for coord_fixed.""" + + def test_coord_fixed_empty_data(self): + """Test coord_fixed with empty data.""" + df = pd.DataFrame({"x": [], "y": []}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + coord_fixed() + fig = plot.draw() # Should not crash + assert fig.layout.yaxis.scaleanchor == "x" + + def test_coord_fixed_very_small_ratio(self): + """Test coord_fixed with very small ratio.""" + df = pd.DataFrame({"x": [0, 1], "y": [0, 1]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + coord_fixed(ratio=0.01) + fig = plot.draw() + assert fig.layout.yaxis.scaleratio == 0.01 + + def test_coord_fixed_very_large_ratio(self): + """Test coord_fixed with very large ratio.""" + df = pd.DataFrame({"x": [0, 1], "y": [0, 1]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + coord_fixed(ratio=100) + fig = plot.draw() + assert fig.layout.yaxis.scaleratio == 100 + + def test_coord_fixed_both_limits(self): + """Test coord_fixed with both x and y limits.""" + df = pd.DataFrame({"x": [0, 1], "y": [0, 1]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + coord_fixed(xlim=(0, 10), ylim=(0, 10)) + ) + fig = plot.draw() + assert fig.layout.xaxis.range is not None + assert fig.layout.yaxis.range is not None + + def test_coord_fixed_no_expand(self): + """Test coord_fixed with expand=False.""" + df = pd.DataFrame({"x": [0, 1], "y": [0, 1]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + coord_fixed(xlim=(0, 1), ylim=(0, 1), expand=False) + ) + fig = plot.draw() + # Without expansion, limits should be exact + xrange = fig.layout.xaxis.range + assert list(xrange) == [0, 1] + + +# ============================================================================= +# INTEGRATION TESTS +# ============================================================================= + + +class TestReverseScaleIntegration: + """Integration tests for reverse scales.""" + + def test_scale_x_reverse_with_line(self): + """Test reversed x-axis with line geom.""" + df = pd.DataFrame({"x": [1, 2, 3, 4, 5], "y": [1, 4, 2, 5, 3]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_line() + scale_x_reverse() + fig = plot.draw() + assert fig.layout.xaxis.autorange == "reversed" + assert len(fig.data) >= 1 + + def test_scale_y_reverse_with_facet(self): + """Test reversed y-axis with faceting.""" + df = pd.DataFrame({ + "x": [1, 2, 1, 2], + "y": [1, 2, 3, 4], + "group": ["A", "A", "B", "B"] + }) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + scale_y_reverse() + + facet_wrap("group") + ) + fig = plot.draw() + # At least one y-axis should be reversed + assert any( + getattr(fig.layout[f"yaxis{'' if i == 0 else i+1}"], "autorange", None) == "reversed" + for i in range(4) + if hasattr(fig.layout, f"yaxis{'' if i == 0 else i+1}") + ) + + def test_reverse_scale_with_multiple_geoms(self): + """Test reversed scale with multiple geoms.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + geom_line() + + scale_x_reverse() + ) + fig = plot.draw() + assert fig.layout.xaxis.autorange == "reversed" + assert len(fig.data) >= 2 + + +class TestCoordFixedIntegration: + """Integration tests for coord_fixed.""" + + def test_coord_fixed_with_circle(self): + """Test that coord_fixed makes a circle look circular.""" + # Create circle data + theta = np.linspace(0, 2 * np.pi, 100) + circle_df = pd.DataFrame({ + "x": np.cos(theta), + "y": np.sin(theta) + }) + plot = ggplot(circle_df, aes(x="x", y="y")) + geom_path() + coord_fixed() + fig = plot.draw() + assert fig.layout.yaxis.scaleratio == 1 + + def test_coord_fixed_with_facet(self): + """Test coord_fixed with faceting.""" + df = pd.DataFrame({ + "x": [1, 2, 1, 2], + "y": [1, 2, 3, 4], + "group": ["A", "A", "B", "B"] + }) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + coord_fixed(ratio=1) + + facet_wrap("group") + ) + fig = plot.draw() + # The first y-axis should have scaleanchor set + assert fig.layout.yaxis.scaleanchor == "x" + + def test_coord_fixed_with_multiple_geoms(self): + """Test coord_fixed with multiple geoms.""" + df = pd.DataFrame({"x": [0, 1, 2], "y": [0, 1, 2]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + geom_line() + + coord_fixed() + ) + fig = plot.draw() + assert fig.layout.yaxis.scaleratio == 1 + assert len(fig.data) >= 2 + + +# ============================================================================= +# VISUAL REGRESSION TESTS +# ============================================================================= + + +class TestVisualRegressionScales: + """Visual regression tests for reverse scales.""" + + def get_axis_signature(self, fig): + """Extract key axis properties.""" + return { + "xaxis": { + "autorange": getattr(fig.layout.xaxis, "autorange", None), + "range": getattr(fig.layout.xaxis, "range", None), + "title": getattr(fig.layout.xaxis.title, "text", None) if fig.layout.xaxis.title else None, + "tickmode": getattr(fig.layout.xaxis, "tickmode", None), + }, + "yaxis": { + "autorange": getattr(fig.layout.yaxis, "autorange", None), + "range": getattr(fig.layout.yaxis, "range", None), + "title": getattr(fig.layout.yaxis.title, "text", None) if fig.layout.yaxis.title else None, + "scaleanchor": getattr(fig.layout.yaxis, "scaleanchor", None), + "scaleratio": getattr(fig.layout.yaxis, "scaleratio", None), + } + } + + def test_scale_x_reverse_signature(self): + """Test scale_x_reverse visual signature.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + scale_x_reverse(name="Reversed", breaks=[1, 2, 3]) + ) + fig = plot.draw() + sig = self.get_axis_signature(fig) + + assert sig["xaxis"]["autorange"] == "reversed" + assert sig["xaxis"]["title"] == "Reversed" + assert sig["xaxis"]["tickmode"] == "array" + + def test_scale_y_reverse_signature(self): + """Test scale_y_reverse visual signature.""" + df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + scale_y_reverse(name="Depth") + ) + fig = plot.draw() + sig = self.get_axis_signature(fig) + + assert sig["yaxis"]["autorange"] == "reversed" + assert sig["yaxis"]["title"] == "Depth" + + def test_coord_fixed_signature(self): + """Test coord_fixed visual signature.""" + df = pd.DataFrame({"x": [0, 1, 2], "y": [0, 1, 2]}) + plot = ggplot(df, aes(x="x", y="y")) + geom_point() + coord_fixed(ratio=1.5) + fig = plot.draw() + sig = self.get_axis_signature(fig) + + assert sig["yaxis"]["scaleanchor"] == "x" + assert sig["yaxis"]["scaleratio"] == 1.5 + + def test_combined_reverse_and_fixed(self): + """Test combining reversed scale with coord_fixed.""" + df = pd.DataFrame({"x": [0, 1, 2], "y": [0, 1, 2]}) + plot = ( + ggplot(df, aes(x="x", y="y")) + + geom_point() + + scale_x_reverse() + + coord_fixed() + ) + fig = plot.draw() + sig = self.get_axis_signature(fig) + + assert sig["xaxis"]["autorange"] == "reversed" + assert sig["yaxis"]["scaleanchor"] == "x" + assert sig["yaxis"]["scaleratio"] == 1 diff --git a/pytest/test_stat_function.py b/pytest/test_stat_function.py index 91d2535..b33173c 100644 --- a/pytest/test_stat_function.py +++ b/pytest/test_stat_function.py @@ -9,12 +9,15 @@ class TestStatFunction: """Tests for stat_function.""" def test_basic_usage(self): - """Test basic function overlay.""" + """Test basic function overlay. + + Note: stat_function creates its own geom_line layer (via geom='line'), + so we don't need to add a separate geom_line(). + """ df = pd.DataFrame({'x': np.random.randn(100)}) plot = (ggplot(df, aes(x='x')) - + stat_function(fun=lambda x: x**2) - + geom_line()) + + stat_function(fun=lambda x: x**2)) fig = plot.draw() assert len(fig.data) >= 1 @@ -24,8 +27,7 @@ def test_scipy_function(self): df = pd.DataFrame({'x': np.random.randn(100)}) plot = (ggplot(df, aes(x='x')) - + stat_function(fun=lambda x: norm.pdf(x, 0, 1)) - + geom_line()) + + stat_function(fun=lambda x: norm.pdf(x, 0, 1))) fig = plot.draw() assert len(fig.data) >= 1 @@ -52,11 +54,13 @@ def test_missing_fun_raises(self): stat_function() def test_no_data_with_xlim(self): - """Test stat_function works without data when xlim is provided.""" + """Test stat_function works without data when xlim is provided. + + stat_function creates its own geom_line layer automatically. + """ from scipy.stats import norm plot = (ggplot() - + stat_function(fun=lambda x: norm.pdf(x), xlim=(-4, 4)) - + geom_line()) + + stat_function(fun=lambda x: norm.pdf(x), xlim=(-4, 4))) fig = plot.draw() assert len(fig.data) >= 1