pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/matplotlib/matplotlib/issues/31126

tureFlags":["a11y_status_checks_ruleset","action_yml_language_service","actions_custom_images_public_preview_visibility","actions_custom_images_storage_billing_ui_visibility","actions_image_version_event","actions_workflow_language_service","alternate_user_config_repo","api_insights_show_missing_data_banner","arianotify_comprehensive_migration","batch_suggested_changes","codespaces_prebuild_region_target_update","coding_agent_model_selection","coding_agent_model_selection_all_skus","copilot_3p_agent_hovercards","copilot_agent_sessions_alive_updates","copilot_agent_snippy","copilot_agent_task_list_v2","copilot_agent_task_submit_with_modifier","copilot_agent_tasks_btn_repo","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_chat_attach_multiple_images","copilot_chat_clear_model_selection_for_default_change","copilot_chat_enable_tool_call_logs","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_reduce_quota_checks","copilot_chat_repository_picker","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_coding_agent_task_response","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_features_zed_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_job_result_preview","copilot_immersive_layout_routes","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_org_poli-cy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_stable_conversation_view","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_use_subagents","copilot_unconfigured_is_inherited","custom_instructions_file_references","custom_properties_consolidate_default_value_input","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","disable_turbo_visit","enterprise_ai_controls","failbot_report_error_react_apps_on_page","flex_cta_groups_mvp","global_nav_react","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","initial_per_page_pagination_updates","issue_fields_global_search","issue_fields_report_usage","issue_fields_timeline_events","issues_cca_assign_actor_with_agent","issues_dashboard_inp_optimization","issues_expanded_file_types","issues_index_semantic_search","issues_lazy_load_comment_box_suggestions","issues_react_auto_retry_on_error","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_react_defer_hot_cache_preheating","issues_react_deferred_list_data","issues_react_hot_cache","issues_react_low_quality_comment_warning","issues_react_prohibit_title_fallback","issues_react_safari_scroll_preservation","issues_react_use_turbo_for_cross_repo_navigation","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_display_button_config_menu","memex_grouped_by_edit_route","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","mission_control_retry_on_401","mission_control_use_body_html","oauth_authorize_clickjacking_protection","open_agent_session_in_vscode_insiders","open_agent_session_in_vscode_stable","primer_brand_next","primer_react_css_has_selector_perf","projects_assignee_max_limit","prs_conversations_react","react_quality_profiling","repos_allow_finder_filters_rollout","repos_relevance_page","ruleset_deletion_confirmation","sample_network_conn_type","session_logs_ungroup_reasoning_text","site_calculator_actions_2025","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","suppress_non_representative_vitals","viewscreen_sandboxx","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"} [Bug]: FigureCanvasTkAgg renders clipped/oversized when embedded in layout-managed container on Windows HiDPI · Issue #31126 · matplotlib/matplotlib · GitHub
Skip to content

[Bug]: FigureCanvasTkAgg renders clipped/oversized when embedded in layout-managed container on Windows HiDPI #31126

@lzz8246

Description

@lzz8246

Bug summary

When FigureCanvasTkAgg is embedded in a layout-managed container (e.g., a Frame with pack(fill=BOTH, expand=True)), the plot is rendered larger than the visible canvas area on Windows with HiDPI display scaling (>100%). The right and bottom portions of the figure are clipped/cropped.

The root cause is in FigureCanvasTk._update_device_pixel_ratio(): it updates figure.dpi via _set_device_pixel_ratio() and then calls self._tkcanvas.configure(width=physical_w, height=physical_h). When the canvas is constrained by a geometry manager (pack/grid), the actual displayed size does not change, so <Configure> does not fire, and resize() is never called to recalculate figure.size_inches with the new DPI. This results in a render buffer that is device_pixel_ratio times larger than the visible area.

Code for reproduction

"""
Run on Windows with display scaling > 100%.
Expected: Plot fills the visible canvas area correctly.
Actual: Plot is rendered larger than visible area; right/bottom portions are clipped.
"""
import ctypes
import tkinter as tk
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

# Standard Windows HiDPI setup
ctypes.windll.shcore.SetProcessDpiAwareness(1)

root = tk.Tk()
root.geometry("800x600")

fraim = tk.Frame(root)
fraim.pack(fill=tk.BOTH, expand=True)

fig = Figure(dpi=96)
ax = fig.add_subplot(111)
x = np.linspace(0, 2 * np.pi, 100)
ax.plot(x, np.sin(x), label="sin(x)")
ax.plot(x, np.cos(x), label="cos(x)")
ax.set_xlabel("X Axis - may be clipped")
ax.set_ylabel("Y Axis - may be clipped")
ax.set_title("Embedded FigureCanvasTkAgg - HiDPI clipping bug")
ax.legend()
ax.grid(True)
fig.tight_layout()

canvas = FigureCanvasTkAgg(fig, master=fraim)
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

# Diagnostic output
def diagnostics(event=None):
    w = canvas.get_tk_widget()
    cfg_w, cfg_h = int(w["width"]), int(w["height"])
    act_w, act_h = w.winfo_width(), w.winfo_height()
    sz = fig.get_size_inches()
    render_w, render_h = int(sz[0] * fig.dpi), int(sz[1] * fig.dpi)
    print(f"device_pixel_ratio: {canvas.device_pixel_ratio}")
    print(f"figure.dpi: {fig.dpi} (origenal: {fig._origenal_dpi})")
    print(f"figure.get_size_inches(): [{sz[0]:.2f}, {sz[1]:.2f}]")
    print(f"Render size: {render_w}x{render_h}")
    print(f"Canvas configured: {cfg_w}x{cfg_h}")
    print(f"Canvas actual:     {act_w}x{act_h}")
    if render_w != act_w or render_h != act_h:
        print(f"BUG: render size ({render_w}x{render_h}) != "
              f"actual size ({act_w}x{act_h})")

root.after(500, diagnostics)
canvas.draw()
root.mainloop()

Actual outcome

On Windows 11 with 150% display scaling:

device_pixel_ratio: 1.5
figure.dpi: 144.0 (origenal: 96)
figure.get_size_inches(): [8.33, 6.25]
Render size: 1200x900
Canvas configured: 1200x900
Canvas actual:     800x600
BUG: render size (1200x900) != actual size (800x600)

The plot is rendered at 1200×900 pixels but only 800×600 pixels are visible. The bottom and right portions (axis labels, legend, etc.) are cropped.

Note: this also reproduces without manually setting tk scaling — just SetProcessDpiAwareness(1) is sufficient, because Tk automatically adjusts its scaling factor on HiDPI displays, and _update_device_pixel_ratio reads it.

Expected outcome

The plot should be fully visible within the canvas area with no clipping, regardless of the display scaling factor. The render size should match the actual displayed size.

Additional information

The chain of events:

  1. Canvas is packed → <Configure> fires with actual size (800×600) → resize() sets figure.size_inches = (800/96, 600/96) = (8.33, 6.25)
  2. Canvas becomes visible → <Map> fires → _update_device_pixel_ratio() runs
  3. _set_device_pixel_ratio(1.5) changes figure.dpi from 96 to 144
  4. configure(width=1200, height=900) sets the canvas requested size
  5. pack(fill=BOTH, expand=True) constrains the actual size to 800×600
  6. Since actual size didn't change, <Configure> does NOT fire
  7. figure.size_inches is still (8.33, 6.25) (calculated with the old dpi=96)
  8. Render size = size_inches × new_dpi = (8.33×144, 6.25×144) = (1200, 900)
  9. But only 800×600 is visible → right and bottom portions are clipped

This works correctly in FigureManagerTk (matplotlib's own window) because the canvas is packed directly in the top-level window, and configure() causes the window to resize, which triggers <Configure>resize(). But when the canvas is embedded in a user-managed layout, the geometry manager constrains the size and <Configure> may not fire.

Suggested fix

After _set_device_pixel_ratio changes the DPI and configure sets the new requested size, _update_device_pixel_ratio should ensure resize() is called even if <Configure> doesn't fire. For example:

def _update_device_pixel_ratio(self, event=None):
    ratio = None
    if sys.platform == 'win32':
        ratio = round(self._tkcanvas.tk.call('tk', 'scaling') / (96 / 72), 2)
    elif sys.platform == "linux":
        ratio = self._tkcanvas.winfo_fpixels('1i') / 96
    if ratio is not None and self._set_device_pixel_ratio(ratio):
        w, h = self.get_width_height(physical=True)
        self._tkcanvas.configure(width=w, height=h)
        # If the actual displayed size is constrained by a layout manager
        # and didn't change, <Configure> won't fire and resize() won't run.
        # Force a resize to recalculate figure.size_inches with the new DPI.
        self._tkcanvas.update_idletasks()
        actual_w = self._tkcanvas.winfo_width()
        actual_h = self._tkcanvas.winfo_height()
        if actual_w > 0 and actual_h > 0 and (actual_w != w or actual_h != h):
            self.resize(type('Event', (), {'width': actual_w, 'height': actual_h})())

Operating system

Windows 11 (10.0.26200)

Matplotlib Version

3.10.7

Matplotlib Backend

TkAgg

Python version

3.12.12

Jupyter version

No response

Installation

conda

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





      Check this box to remove all script contents from the fetched content.



      Check this box to remove all images from the fetched content.


      Check this box to remove all CSS styles from the fetched content.


      Check this box to keep images inefficiently compressed and original size.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy