+ Select a tag from the sidebar to view its data. +
+ + +diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 934eb1f..6bd2490 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,95 +1,33 @@ -# # .github/workflows/docs.yml -# name: Deploy Documentation - -# on: -# push: -# tags: -# - 'v*.*.*' -# workflow_dispatch: # Allow manual triggering - -# permissions: -# contents: write # This is the key addition - explicitly grant write permission - -# jobs: -# deploy: -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v3 -# with: -# fetch-depth: 0 # fetch all history for proper versioning - -# - name: Set up Python -# uses: actions/setup-python@v4 -# with: -# python-version: '3.10' - -# - name: Install dependencies -# run: | -# python -m pip install --upgrade pip -# pip install -e ".[dev]" - -# - name: Set up Git user -# run: | -# git config --local user.email "github-actions[bot]@users.noreply.github.com" -# git config --local user.name "github-actions[bot]" - -# - name: Extract version from tag -# id: get_version -# run: | -# echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - -# - name: Deploy docs -# run: | -# mike deploy --push --update-aliases ${{ env.VERSION }} latest - -# .github/workflows/docs.yml -name: Deploy Documentation - +name: Documentation on: - workflow_run: - workflows: ["Publish Package"] - types: - - completed + push: branches: + - master - main - workflow_dispatch: # Allow manual triggering - permissions: - contents: write - + contents: read + pages: write + id-token: write jobs: deploy: - # Only run if the package publish was successful - if: ${{ github.event.workflow_run.conclusion == 'success' }} + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/configure-pages@v5 + - uses: actions/checkout@v5 + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - fetch-depth: 0 # fetch all history for proper versioning - - - name: Set up Python - uses: actions/setup-python@v4 + enable-cache: true + - name: Install Python 3.14 + run: uv python install 3.14 + - name: Generate API reference pages + run: uv run --python 3.14 --group dev python scripts/gen_ref_pages.py + - run: uv run --python 3.14 --group dev zensical build --clean + - uses: actions/upload-pages-artifact@v4 with: - python-version: '3.11' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e ".[dev]" - - - name: Set up Git user - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - - - name: Get latest tag - id: get_tag - run: | - # Get the latest tag - TAG=$(git describe --tags `git rev-list --tags --max-count=1`) - echo "TAG=${TAG}" >> $GITHUB_ENV - echo "VERSION=${TAG#v}" >> $GITHUB_ENV - - - name: Deploy docs - run: | - mike deploy --push --update-aliases ${{ env.VERSION }} latest \ No newline at end of file + path: site + - uses: actions/deploy-pages@v4 + id: deployment diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 45ad880..ef2b0b2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,35 +2,97 @@ name: Publish Package on: push: - tags: - - 'v*' # Trigger on version tags (v0.1.0, v1.0.0, etc.) + branches: ["main"] # Run tests on every push to main + tags: ["v*"] # Run tests AND publish on tags + pull_request: + branches: ["main"] # Run tests on PRs -# Add this permissions block permissions: contents: read - id-token: write # This is needed for PyPI's trusted publishing + id-token: write jobs: - build-and-publish: + test: + name: "Test (Python ${{ matrix.python-version }})" runs-on: ubuntu-latest - environment: pypi + strategy: + matrix: + python-version: ["3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Run unit tests + run: uv run --python ${{ matrix.python-version }} --group dev pytest + + - name: Generate coverage report + if: matrix.python-version == '3.14' && github.event_name == 'push' && github.ref == 'refs/heads/main' + run: uv run --python ${{ matrix.python-version }} --group dev pytest --cov=stochas --cov-report=xml + + - name: Generate coverage badge + if: matrix.python-version == '3.14' && github.event_name == 'push' && github.ref == 'refs/heads/main' + run: uv run --python ${{ matrix.python-version }} --group dev genbadge coverage -i coverage.xml -o docs/assets/coverage-badge.svg + + - name: Commit coverage badge + if: matrix.python-version == '3.14' && github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: update coverage badge" + file_pattern: docs/assets/coverage-badge.svg + + publish: + name: "Build and Publish" + needs: [test] + # ONLY run this job if the push was actually a tag + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + environment: + name: pypi + permissions: + id-token: write # Required for PyPI Trusted Publishing + contents: write # Required to create GitHub Release and upload assets + steps: + - name: Checkout + uses: actions/checkout@v6 with: - python-version: '3.11' - - - name: Install build dependencies + # Fetch all history so we can see other branches + fetch-depth: 0 + + - name: Verify Tag is on Main + # Only perform this check if we were triggered by a tag + if: startsWith(github.ref, 'refs/tags/v') run: | - python -m pip install --upgrade pip - pip install build - - - name: Build package - run: python -m build - + # Check if 'main' branch contains the current commit + if ! git branch -r --contains ${{ github.sha }} | grep -q "origin/main"; then + echo "::error::Tag ${{ github.ref_name }} was pushed on a non-main branch. Skipping workflow." + exit 1 + fi + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Install Python 3.14 + run: uv python install 3.14 + + - name: Build + run: uv build + - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - # No need to specify token when using trusted publishing \ No newline at end of file + run: uv publish --check-url https://pypi.org/simple + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: | + dist/*.whl + dist/*.tar.gz diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 9e38e4a..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,30 +0,0 @@ -# .github/workflows/test.yml -name: Run Tests - -on: - push: - branches: [ dev, main ] - pull_request: - branches: [ main ] - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.11", "3.12", "3.13"] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest pytest-cov - pip install -e ".[dev]" - - name: Run tests - run: | - pytest tests/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 636636b..2d5e7f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,38 @@ -**/workdir/ -**/__pycache__/**/* -**/*.egg*/ -**/*.egg-info/** -**/*.pyc -**/*.pyo -**/*.pyd -**/*.pyz -**/*.code-workspace -dist/ +# Python-generated files +__pycache__/ +*.py[oc] build/ -trendify.log +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +mojo.log +mojo.log.* + +site +docs/reference/ +mojo-models +textures + +typings + +# TypeScript build artifacts (node_modules is dev-only; compiled .js files ARE committed) +node_modules/ +*.js.map + +.coverage + +**/*.code-workspace + +trendify.log* + # Test output files discriminator_tests.json discriminator_tests.log -.coverage -coverage.xml -# MkDocs build directory -site/ \ No newline at end of file + +/**/.DS_Store + +*.prof diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8d86721 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-added-large-files + args: ["--maxkb=500"] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.17 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.11.21 + hooks: + - id: uv-lock + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.11.21 + hooks: + - id: uv-export + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.411 + hooks: + - id: pyright + - id: pyright + name: Pyright (docs examples) + args: [docs] + pass_filenames: false + always_run: true + stages: [pre-commit] + - repo: local + hooks: + - id: act-test + name: Run GitHub Actions locally (act) + entry: act -j test + language: system + stages: [pre-push] + pass_filenames: false + files: ^(src/|tests/|pyproject\.toml|uv\.lock) diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/README.md b/README.md index 19727f0..4032aad 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,65 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
If you are not re-directed to docs page in 5 seconds, click here.
- \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index bac9286..f5891ad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,198 +1,220 @@ -site_name: Trendify +site_name: trendify site_url: https://talbotknighton.github.io/trendify/ -repo_url: https://github.com/TalbotKnighton/trendify -repo_name: TalbotKnighton/trendify -site_description: Data visualization for batch processes via data models. -site_author: Talbot Knighton +site_author: Talbot Knighton and David Gable +copyright: Copyright © 2026 Talbot Knighton and David Gable +repo_url: https://github.com/talbotknighton/trendify +repo_name: trendify +remote_branch: gh-pages +remote_name: origin -# edit_uri: blob/master/docs/ +hooks: + - docs/hooks.py + +watch: + - docs + - src + - scripts + +nav: + - Home: index.md + - User Guide: user-guide.md + - Code Reference: + - reference/trendify/index.md + - reference/trendify/cli.md + - reference/trendify/color.md + - reference/trendify/examples.md + - reference/trendify/log.md + - reference/trendify/pipeline.md + - reference/trendify/typing.md + - Base: + - reference/trendify/base/index.md + - reference/trendify/base/helpers.md + - reference/trendify/base/pen.md + - reference/trendify/base/record.md + - Formats: + - reference/trendify/formats/index.md + - reference/trendify/formats/format2d.md + - reference/trendify/formats/table.md + - Generator: + - reference/trendify/generator/index.md + - reference/trendify/generator/generate.md + - reference/trendify/generator/histogrammer.md + - reference/trendify/generator/render.md + - reference/trendify/generator/table_builder.md + - reference/trendify/generator/xy_data_plotter.md + - Plotting: + - reference/trendify/plotting/index.md + - reference/trendify/plotting/axline.md + - reference/trendify/plotting/figure.md + - reference/trendify/plotting/histogram.md + - reference/trendify/plotting/point.md + - reference/trendify/plotting/scatter.md + - reference/trendify/plotting/trace.md + - Store: + - reference/trendify/store/index.md + - reference/trendify/store/db.md + - reference/trendify/store/record_store.md + - reference/trendify/store/tags.md + - Styling: + - reference/trendify/styling/index.md + - reference/trendify/styling/grid.md + - reference/trendify/styling/legend.md + - reference/trendify/styling/marker.md + - Viewer: + - reference/trendify/viewer/index.md + - reference/trendify/viewer/app.md + - reference/trendify/viewer/plot_config.md + - reference/trendify/viewer/tag_tree.md + - Routes: + - reference/trendify/viewer/routes/index.md + - reference/trendify/viewer/routes/api.md + - reference/trendify/viewer/routes/pages.md theme: name: material - logo: assets/logo_pink_white_bg.svg - favicon: assets/logo_pink_white_bg.svg - features: - # - header.autohide - - search.suggest - - search.highlight - - navigation.tabs - - navigation.top - - navigation.tabs - - navigation.tabs.sticky - - navigation.sections - - navigation.tracking - # - navigation.indexes - - content.tabs - - content.tabs.link - - content.code.annotation - - content.code.copy - - content.tooltips - # - toc.integrate - color_mode: auto - user_color_mode_toggle: true - language: en + logo: assets/main-logo.svg + favicon: assets/main-logo-white-bg.svg + font: false + palette: - - scheme: default + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + accent: custom toggle: - icon: material/toggle-switch-off-outline + icon: material/lightbulb-on name: Switch to dark mode - primary: pink - accent: blue - - scheme: slate + + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + accent: custom toggle: - icon: material/toggle-switch + icon: material/lightbulb-outline name: Switch to light mode - primary: pink - accent: blue - highlightjs: true - hljs_languages: - - yaml - - django - -nav: - - Welcome: index.md - - Motivation: motivation.md - - Examples: - - Basic: example.md - - More: more_examples.md - - Recipe: recipe.md - - API and CLI: api_and_cli.md - - Code Reference: reference/ - - Planned Features: planned_features.md -# Add the extra section here -extra: - # Add your custom configurations or variables - disable_ssl_certificate_validation: true - -extra_css: - - css/code_select.css - - css/mkdocstrings.css - - css/logo.css + features: + - navigation.tabs + - navigation.instant + # - navigation.instant.preview + - navigation.instant.prefetch + - navigation.instant.progress + - navigation.top + - navigation.path + - navigation.sections + - navigation.tabs.sticky + - navigation.footer + - content.action.edit + - content.action.view + - content.code.copy + - content.code.select + - content.code.annotate + - content.tooltips + - search.highlight + - search.suggest + - search.share + - content.tabs.link + - content.footnote.tooltips -# exclude_docs: | -# *.py +plugins: + - search + - open-in-new-tab: + add_icon: true + # gen-files and literate-nav are MkDocs-only plugins; zensical ignores them. + # Reference pages are pre-generated by scripts/gen_ref_pages.py instead. + # - gen-files: + # scripts: + # - scripts/gen_ref_pages.py + # - literate-nav: + # nav_file: SUMMARY.md + # - section-index + - mkdocstrings: + handlers: + python: + paths: [src] + options: + modernize_annotations: true + backlinks: tree + docstring_options: + ignore_init_summary: true + merge_init_into_class: true + show_root_heading: true + show_root_full_path: false + show_source: true + show_bases: true + members_order: source + show_symbol_type_heading: true + show_symbol_type_toc: true + show_signature_annotations: true + separate_signature: true + summary: false # set to true to get summary tables + inherited_members: true + # inheritance_diagram_direction: TD + # show_inheritance_diagram: true + show_attribute_values: true + signature_crossrefs: true + extensions: + - pydantic: { schema: true } + inventories: + - https://docs.python.org/3/objects.inv + - https://mkdocstrings.github.io/griffe/objects.inv + - https://docs.pydantic.dev/latest/objects.inv + - https://numpy.org/doc/stable/objects.inv markdown_extensions: - - toc: - permalink: true + - abbr - admonition - - codehilite - - toc: - permalink: true - title: On this page - attr_list + - md_in_html - def_list - - tables + - pymdownx.arithmatex: + generic: true + - pymdownx.tasklist: + custom_checkbox: true + - toc: + permalink: true - pymdownx.highlight: - use_pygments: false + anchor_linenums: true + line_spans: __span pygments_lang_class: true + auto_title: true + linenums: true + - pymdownx.inlinehilite + - pymdownx.details - pymdownx.snippets - - pymdownx.superfences - - pymdownx.tabbed: - alternate_style: true - - callouts - - mdx_gh_links: - user: mkdocs - repo: mkdocs - - mkdocs-click - # - - pymdownx.snippets: - base_path: "docs/" - auto_append: - - includes/abbreviations.md + - pymdownx.blocks.caption - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format - - abbr - - attr_list - - md_in_html - - def_list - - pymdownx.highlight: - anchor_linenums: true - - pymdownx.inlinehilite - - pymdownx.tasklist: - custom_checkbox: true - clickable_checkbox: false - - admonition - - pymdownx.arithmatex: - generic: true - - footnotes - - pymdownx.details - - pymdownx.superfences - pymdownx.tabbed: alternate_style: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - tables + - footnotes + - pymdownx.critic + - pymdownx.caret + - pymdownx.keys - pymdownx.mark - - markdown_include.include: - base_path: docs - -copyright: Copyright © 2024 Talbot Knighton, Maintained by the Talbot Knighton. + - pymdownx.tilde + - zensical.extensions.glightbox: + auto_themed: true + - zensical.extensions.macros -hooks: - - docs/hooks.py +extra: + generator: false -plugins: - # - git-revision-date-localized: - # enable_creation_date: true - - search - - glightbox - - gen-files: - scripts: - - scripts/gen_ref_pages.py - - section-index - - autorefs - - literate-nav: - nav_file: SUMMARY.md - implicit_index: true - - mkdocstrings: - handlers: - python: - import: - # https://stackoverflow.com/questions/52805115/certificate-verify-failed-unable-to-get-local-issuer-certificate - - https://docs.python.org/3/objects.inv - - https://matplotlib.org/objects.inv - - https://numpy.org/doc/stable/objects.inv - - https://pandas.pydata.org/docs/objects.inv - - https://docs.scipy.org/doc/scipy/objects.inv - - https://numpydantic.readthedocs.io/en/latest/objects.inv - - https://docs.pydantic.dev/latest/objects.inv - options: - extensions: - - griffe_pydantic: - schema: true - docstring_style: google - docstring_section_style: table - members_order: alphabetical - show_root_heading: true - show_source: true - separate_signature: true - show_signature: true - show_signature_annotations: true - signature_crossrefs: true - # - merge_init_into_class: true - show_if_no_docstring: false - # annotations_path: full - show_docstring_functions: true - heading_level: 2 - line_length: 100 - show_root_toc_entry: false - show_root_full_path: false - inherited_members: false - show_submodules: false - show_docstring_classes: true - docstring_options: - ignore_init_summary: false - paths: [src] - # - - exclude: - glob: - - snippets/* - - snippets +extra_javascript: + - javascripts/katex.js + - https://unpkg.com/katex@0/dist/katex.min.js + - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js + - https://unpkg.com/mermaid@10.9.0/dist/mermaid.min.js + - https://unpkg.com/tablesort@5.3.0/dist/tablesort.min.js + - javascripts/tablesort.js -watch: - - src/ - - scripts/ - - docs/ +extra_css: + - css/extra.css + - https://unpkg.com/katex@0/dist/katex.min.css diff --git a/plotly_examples/histogram_1.py b/plotly_examples/histogram_1.py deleted file mode 100644 index 7c35d28..0000000 --- a/plotly_examples/histogram_1.py +++ /dev/null @@ -1,905 +0,0 @@ -"""Interactive histogram visualization with interactive legend features.""" -import dash -from dash import dcc, html -from dash.dependencies import Input, Output, State, MATCH, ALL -import plotly.graph_objects as go -import pandas as pd -import numpy as np -from typing import Dict, Any - -# Debug Configuration -PYTHON_DEBUG = True -MIN_BINS = 1 - -def debug_log(message: str) -> None: - """Python-side debug logging.""" - if PYTHON_DEBUG: - print(f"[PY] {message}") - -def calculate_histogram_statistics(series: pd.Series, num_bins: int = 20) -> Dict[str, Any]: - """Calculate statistics for a histogram series.""" - try: - # Drop NA values - clean_series = series.dropna() - - if len(clean_series) < 2: - return { - "samples": len(clean_series), - "missing": len(series) - len(clean_series), - "mean": "N/A" if len(clean_series) == 0 else f"{clean_series.mean():.3f}", - "bins": num_bins - } - - stats_dict = { - "samples": len(clean_series), - "missing": len(series) - len(clean_series), - "mean": f"{clean_series.mean():.3f}", - "median": f"{clean_series.median():.3f}", - "std_dev": f"{clean_series.std():.3f}", - "min": f"{clean_series.min():.3f}", - "max": f"{clean_series.max():.3f}", - "bins": num_bins, - "bin_width": f"{(clean_series.max() - clean_series.min()) / num_bins:.3f}" if clean_series.max() != clean_series.min() else "N/A" - } - - # Add skewness and kurtosis if enough data points - if len(clean_series) > 3: - try: - stats_dict["skewness"] = f"{clean_series.skew():.3f}" - stats_dict["kurtosis"] = f"{clean_series.kurtosis():.3f}" - except: - pass - - return stats_dict - except Exception as e: - return {"error": str(e), "bins": num_bins} - -# Create sample data -df = pd.DataFrame({ - 'A': np.random.normal(0, 1, 1000), - 'B': np.random.normal(2, 1.5, 1000), - 'C': np.random.exponential(2, 1000), - 'D': np.random.gamma(2, 2, 1000), - 'Category': np.random.choice(['X', 'Y', 'Z'], 1000) -}) - -# Initialize the Dash app -app = dash.Dash(__name__, suppress_callback_exceptions=True) - -# Styles -dropdown_style = { - 'width': '100%', - 'marginBottom': '20px', - 'zIndex': 999, -} - -info_panel_style = { - 'position': 'fixed', # Fixed positioning for hover panel - 'backgroundColor': 'white', - 'padding': '15px', - 'borderRadius': '5px', - 'boxShadow': '0 0 10px rgba(0,0,0,0.3)', - 'zIndex': 1000, - 'display': 'none', - 'minWidth': '200px', - 'maxWidth': '300px', - 'fontSize': '12px' -} - -color_picker_style = { - 'position': 'fixed', - 'top': '50%', - 'left': '50%', - 'transform': 'translate(-50%, -50%)', - 'backgroundColor': 'white', - 'padding': '20px', - 'borderRadius': '5px', - 'boxShadow': '0 0 10px rgba(0,0,0,0.3)', - 'zIndex': 1001, - 'display': 'none' -} - -# Available colors -COLORS = [ - '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', - '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', - '#aec7e8', '#ffbb78', '#98df8a', '#ff9896', '#c5b0d5', - '#c49c94', '#f7b6d2', '#c7c7c7', '#dbdb8d', '#9edae5' -] - -# Define the layout -app.layout = html.Div([ - html.H1("Interactive Histogram Visualization"), - - # Control panel for column selection - html.Div([ - html.Div([ - html.H3("Select Columns for Histograms"), - dcc.Dropdown( - id='histogram-columns', - options=[{'label': col, 'value': col} for col in df.columns if col != 'Category'], - value=[], - multi=True, - style=dropdown_style - ), - ], style={'width': '45%', 'display': 'inline-block', 'margin': '5px'}), - - ]), - - # Graph and custom legend - html.Div([ - html.Div([ - html.H3("Histogram Settings"), - dcc.Checklist( - id='histogram-settings', - options=[ - {'label': ' Show KDE (density curve)', 'value': 'kde'}, - {'label': ' Normalize histograms', 'value': 'normalize'}, - {'label': ' Show rug plot', 'value': 'rug'} - ], - value=['kde'], - style={'display': 'block', 'gap': '10px'}, - inputStyle={'marginRight': '5px'}, - labelStyle={'display': 'block', 'marginBottom': '10px'} - ), - html.Div([ - html.Label("Global Number of Bins:"), - dcc.Slider( - id='bins-slider', - min=MIN_BINS, - max=100, - step=1, - value=20, - marks={i: str(i) for i in [1, 20, 40, 60, 80, 100]}, - ) - ], style={'marginTop': '15px'}), - html.Div([ - html.Label("Lock all bins to global setting:", - style={'display': 'inline-block', 'marginRight': '10px'}), - dcc.Checklist( - id='global-bins-toggle', - options=[{'label': '', 'value': 'enabled'}], - value=['enabled'], # Enabled by default - inline=True, - inputStyle={'marginRight': '5px'} - ) - ], style={'marginTop': '15px', 'marginBottom': '5px'}), - ], style={'width': '20%', 'display': 'inline-block', 'margin': '5px'}), - # Main plot - html.Div([ - dcc.Graph( - id='histogram-plot', - style={'height': '700px'} - ), - ], style={'width': '50%', 'display': 'inline-block', 'verticalAlign': 'top'}), - - # Custom legend - html.Div([ - html.H3("Series"), - html.Div(id='custom-legend', style={ - 'overflowY': 'auto', - 'maxHeight': '600px' - }) - ], style={'width': '20%', 'display': 'inline-block', 'verticalAlign': 'top'}), - - ]), - - # Info panel (hidden by default) - html.Div(id='series-info-panel', style=info_panel_style), - - # Color picker panel (hidden by default) - html.Div([ - html.H3("Select Color"), - html.Div( - [html.Button( - style={'backgroundColor': color, 'width': '30px', 'height': '30px', 'margin': '5px'}, - id={'type': 'color-option', 'index': i} - ) for i, color in enumerate(COLORS)], - style={'display': 'grid', 'gridTemplateColumns': 'repeat(5, 1fr)', 'gap': '5px'} - ), - html.Button("Close", id='close-color-picker', style={'marginTop': '10px'}) - ], id='color-picker-panel', style=color_picker_style), - - # Store components - dcc.Store(id='series-colors'), - dcc.Store(id='series-data'), - dcc.Store(id='active-series'), - dcc.Store(id='bins-settings'), - dcc.Store(id='global-bins-active', data=True), # True by default# Add this to your layout -dcc.Interval(id='interval-component', interval=60*1000, n_intervals=0) # Just a trigger -]) - -# Update whether global bins control is active based on toggle -@app.callback( - Output('global-bins-active', 'data'), - [Input('global-bins-toggle', 'value')] -) -def update_global_bins_state(toggle_value): - """Update whether global bins control is active based on toggle.""" - return 'enabled' in toggle_value if toggle_value else False - -# # Initialize series-colors and statistics when columns are selected -# @app.callback( -# [Output('series-colors', 'data'), -# Output('series-data', 'data')], -# [Input('histogram-columns', 'value'), -# Input('bins-settings', 'data')], -# [State('series-colors', 'data')] -# ) -# def initialize_series_data(selected_cols, bins_settings, existing_colors): -# """Initialize color data and statistics for selected columns.""" -# debug_log("Initializing series data") -# color_data = existing_colors or {} -# series_stats = {} -# bins_settings = bins_settings or {} - -# if not selected_cols: -# return color_data, series_stats - -# for col in selected_cols: -# series_name = f'Histogram of {col}' - -# # Assign a color if not already assigned -# if series_name not in color_data: -# color_data[series_name] = COLORS[len(color_data) % len(COLORS)] - -# # Get bin count (default 20) -# num_bins = bins_settings.get(series_name, 20) - -# # Calculate statistics with the current bin count -# stats = calculate_histogram_statistics(df[col], num_bins) -# series_stats[series_name] = stats - -# # Keep only the colors for selected columns -# color_data = {k: v for k, v in color_data.items() -# if any(f'Histogram of {x}' == k for x in selected_cols)} - -# return color_data, series_stats - - -# Update custom legend with bin controls -@app.callback( - Output('custom-legend', 'children'), - [Input('histogram-columns', 'value'), - Input('series-colors', 'data'), - Input('bins-settings', 'data')] -) -def update_legend_with_bin_controls(selected_cols, color_data, bins_settings): - """Update custom legend with interactive elements including bin controls.""" - legend_items = [] - bins_settings = bins_settings or {} - - if not selected_cols or not color_data: - return legend_items - - for col in selected_cols: - series_name = f'Histogram of {col}' - color = color_data.get(series_name, COLORS[0]) - - # Get bin count for this series (default: 20 bins) - bin_count = bins_settings.get(series_name, 20) - - legend_items.append(html.Div([ - # Color button - html.Button( - style={ - 'backgroundColor': color, - 'width': '20px', - 'height': '20px', - 'marginRight': '10px', - 'verticalAlign': 'middle', - 'cursor': 'pointer', - 'border': '1px solid #ddd' - }, - id={'type': 'color-button', 'index': series_name} - ), - # Series name - html.Span( - series_name, - style={ - 'cursor': 'default', - 'userSelect': 'none', - 'display': 'inline-block', - 'marginRight': '10px' - } - ), - # Bin control - html.Div([ - html.Label("Bins:", style={'fontSize': '11px', 'marginRight': '5px'}), - dcc.Input( - id={'type': 'bin-input', 'index': series_name}, - type="number", - min=5, - max=100, - step=1, - value=bin_count, - style={'width': '50px', 'fontSize': '11px'}, - debounce=False, # Process changes immediately - persistence=True # Remember values - ) - ], style={'display': 'inline-block'}) - ], - className='legend-item', - id={'type': 'legend-item', 'index': series_name}, - **{ - 'data-series': series_name, - 'style': { - 'margin': '10px', - 'padding': '5px', - 'borderRadius': '3px', - 'display': 'flex', - 'alignItems': 'center' - } - })) - - return legend_items - -@app.callback( - [Output('color-picker-panel', 'style'), - Output('active-series', 'data')], - [Input({'type': 'color-button', 'index': ALL}, 'n_clicks'), - Input('close-color-picker', 'n_clicks')], - [State({'type': 'color-button', 'index': ALL}, 'id'), - State('color-picker-panel', 'style')] -) -def toggle_color_picker(button_clicks, close_clicks, button_ids, current_style): - """Show/hide color picker when clicking color buttons.""" - ctx = dash.callback_context - if not ctx.triggered: - return dict(current_style, display='none'), None - - triggered_id = ctx.triggered[0]['prop_id'] - - # Close on close button click - if 'close-color-picker' in triggered_id: - debug_log("Closing color picker") - return dict(current_style, display='none'), None - - # Open on color button click - if 'color-button' in triggered_id and button_clicks and any(button_clicks): - try: - # Find which button was clicked - button_idx = next((i for i, clicks in enumerate(button_clicks) if clicks), None) - if button_idx is not None: - series_name = button_ids[button_idx]['index'] - debug_log(f"Opening color picker for {series_name}") - return dict(current_style, display='block'), series_name - except Exception as e: - debug_log(f"Error in toggle_color_picker: {e}") - - return dict(current_style, display='none'), None - -@app.callback( - Output('series-colors', 'data', allow_duplicate=True), - [Input({'type': 'color-option', 'index': ALL}, 'n_clicks')], - [State({'type': 'color-option', 'index': ALL}, 'id'), - State('active-series', 'data'), - State('series-colors', 'data')], - prevent_initial_call=True -) -def update_series_color(color_clicks, color_ids, active_series, current_colors): - """Update color when selecting from color picker.""" - ctx = dash.callback_context - if not ctx.triggered or not active_series or not current_colors: - return dash.no_update - - triggered_id = ctx.triggered[0]['prop_id'] - if 'color-option' not in triggered_id or not any(click for click in color_clicks if click): - return dash.no_update - - try: - # Find which color was clicked - color_idx = next((i for i, clicks in enumerate(color_clicks) if clicks), None) - if color_idx is not None: - current_colors[active_series] = COLORS[color_idx] - debug_log(f"Updated color for {active_series} to {COLORS[color_idx]}") - return current_colors - except Exception as e: - debug_log(f"Error in update_series_color: {e}") - - return dash.no_update - -@app.callback( - [Output('color-picker-panel', 'style', allow_duplicate=True), - Output('active-series', 'data', allow_duplicate=True)], - [Input({'type': 'color-option', 'index': ALL}, 'n_clicks')], - [State('color-picker-panel', 'style')], - prevent_initial_call=True -) -def close_color_picker_after_selection(color_clicks, current_style): - """Close the color picker after a color is selected.""" - if any(click for click in color_clicks if click): - return dict(current_style, display='none'), None - return dash.no_update, dash.no_update - -@app.callback( - Output('histogram-plot', 'figure'), - [Input('histogram-columns', 'value'), - Input('histogram-settings', 'value'), - Input('series-colors', 'data'), - Input('bins-settings', 'data')] -) -def update_histogram(selected_cols, settings, color_data, bins_settings): - """Update the histogram based on selected columns and settings.""" - debug_log("Updating histogram plot") - fig = go.Figure() - - if not selected_cols or not color_data: - fig.add_annotation( - text="Please select columns for the histogram", - xref="paper", yref="paper", - x=0.5, y=0.5, showarrow=False - ) - return fig - - normalize = 'normalize' in settings - show_kde = 'kde' in settings - show_rug = 'rug' in settings - bins_settings = bins_settings or {} - - for col in selected_cols: - series_name = f'Histogram of {col}' - color = color_data.get(series_name) - if not color: # Skip if no color assigned - continue - - # Get bins setting for this series (default: 20) - num_bins = bins_settings.get(series_name, 20) - - # Clean the data - data = df[col].dropna() - - # Add histogram with precisely num_bins bins - histnorm = 'probability' if normalize else None - - # Calculate bin range directly - min_val, max_val = min(data), max(data) - bin_width = (max_val - min_val) / num_bins if max_val > min_val else 1 - - # Use xbins to control exactly how many bins are displayed - fig.add_trace(go.Histogram( - x=data, - name=series_name, - histnorm=histnorm, - marker_color=color, - opacity=0.7, - showlegend=False, - xbins=dict( - start=min_val, - end=max_val, - size=bin_width - ), - autobinx=False # Disable autobinning - )) - - # Add KDE if requested - if show_kde and len(data) > 1: - try: - # Calculate KDE values - from scipy import stats as scipy_stats - - # Make sure we have enough unique values for KDE - if len(np.unique(data)) > 5: - kde = scipy_stats.gaussian_kde(data) - x_range = np.linspace(min(data), max(data), 200) - y_range = kde(x_range) - - # Scale KDE to match histogram height - if normalize: - # Scale for normalized histogram - y_range = y_range / np.trapz(y_range, x_range) - else: - # Scale for count histogram - hist, edges = np.histogram(data, bins=num_bins) - max_hist_height = np.max(hist) - max_kde_height = np.max(y_range) - scale_factor = max_hist_height / max_kde_height if max_kde_height > 0 else 1 - y_range = y_range * scale_factor - - fig.add_trace(go.Scatter( - x=x_range, - y=y_range, - mode='lines', - name=f'KDE of {col}', - line=dict(color=color, width=2), - showlegend=False - )) - except Exception as e: - debug_log(f"Error computing KDE: {e}") - - # Add rug plot if requested - if show_rug: - try: - # Create a small y value for the rug - min_y = 0 - if normalize: - # Position below x-axis for normalized histogram - min_y = -0.02 - - fig.add_trace(go.Scatter( - x=data, - y=[min_y] * len(data), - mode='markers', - marker=dict( - symbol='line-ns', - size=10, - color=color, - opacity=0.5 - ), - showlegend=False, - hoverinfo='x' - )) - except Exception as e: - debug_log(f"Error adding rug plot: {e}") - - # Display histograms using overlay mode - fig.update_layout( - barmode='overlay', - title='Interactive Histogram Visualization', - plot_bgcolor='white', - paper_bgcolor='white', - showlegend=False, # Using custom legend - xaxis=dict( - title='Value', - showgrid=True, - gridwidth=1, - gridcolor='LightGray', - zeroline=True, - zerolinewidth=2, - zerolinecolor='LightGray' - ), - yaxis=dict( - title='Frequency' if not normalize else 'Probability', - showgrid=True, - gridwidth=1, - gridcolor='LightGray', - zeroline=True, - zerolinewidth=2, - zerolinecolor='LightGray' - ), - height=700, - margin=dict(t=50, b=50, l=50, r=50) - ) - - debug_log("Histogram plot updated") - return fig - -app.clientside_callback( - """ - function(children, series_data) { - if (!children) return window.dash_clientside.no_update; - - const JS_DEBUG = true; - - function debugLog(message) { - if (JS_DEBUG) console.log('[JS]', message); - } - - // Global reference to track which element is being hovered - window.activeHoverLegendItem = window.activeHoverLegendItem || null; - - function updateStatsPanel() { - // If no item is active, don't update - if (!window.activeHoverLegendItem) return; - - const panel = document.getElementById('series-info-panel'); - if (!panel) return; - - const series = window.activeHoverLegendItem.getAttribute('data-series'); - - // If panel is visible, update its content - if (panel.style.display === 'block' && series_data && series_data[series]) { - let statsHtml = "| ${key} | -${value} | -
| ${key} | -${value} | -
No statistics available for this series.
"; - } - - statsHtml += "+ Select a tag from the sidebar to view its data. +
+ + +No data available for this view.
+No lines match the current metadata filter.
+No data available for this view.
+ +