{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Quickstart Guide" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This guide introduces the core concept and components of `outset` plots, then covers how to create them using the `outset.OutsetGrid` interface.\n", "\n", "The `outset.OutsetGrid` class provides a `FacetGrid`-like interface to arrange magnifications of plotted data on an axes grid.\n", "With this API, magnification frame positioning is *automatically determined* based on the structure of dataframe content.\n", "\n", "The `FacetGrid` class also supports *manual specification* of magnified areas, covered subsequently.\n", "This interface broadcasts identical plot content over all grid axes then sets individual axes' data limits to create magnification panels.\n", "\n", "The final sections of the guide cover detail *aspect ratio management*, *layout control*, and *styling* --- including how to use `outset.inset_outsets` to transform an `OutsetGrid` to render magnification plots as inset axes over the main plot, instead of abreast in a grid with the main plot.\n", "In addition to `outset.OutsetGrid`, lower-level `outset.marqueeplot` and `outset.draw_marquee` APIs can be used directly for more bespoke applications." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Table of Contents\n", "\n", "- [Taxonomy: Marquees, Outsets, and Insets](#Taxonomy:-Marquees,-Outsets,-and-Insets)\n", "- [Inferred Zoom Areas](#Inferred-Zoom-Areas)\n", "- [Explicit Zoom Areas](#Explicit-Zoom-Areas)\n", "- [Aspect Ratio](#Aspect-Ratio)\n", "- [Layout Control](#Layout-Control)\n", "- [Axes-level Interfaces](#Axes-level-Interfaces)\n", "- [Further Subjects](#Further-Subjects)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from matplotlib import cbook as mpl_cbook\n", "from matplotlib import pyplot as plt\n", "import numpy as np\n", "import outset as otst\n", "from outset import mark as otst_mark\n", "from outset import patched as otst_patched\n", "from outset import tweak as otst_tweak\n", "from outset import util as otst_util\n", "import pandas as pd\n", "import seaborn as sns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Some imports and setup." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Taxonomy: Marquees, Outsets, and Insets" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Data Preparation ---\n", "# Create a DataFrame with sample data defining the extent of marquee elements\n", "plot_data = pd.DataFrame(\n", " {\n", " \"x_values\": [0, 2, 5.8, 6],\n", " \"y_values\": [1, 4, 8, 9],\n", " \"group\": [\"group1\", \"group1\", \"group2\", \"group2\"],\n", " }\n", ")\n", "\n", "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object for creating plots with grouped data.\n", "outset_grid = otst.OutsetGrid( # API is a la FacetGrid\n", " data=plot_data,\n", " x=\"x_values\",\n", " y=\"y_values\",\n", " col=\"group\",\n", " hue=\"group\",\n", " marqueeplot_kws={\"mark_glyph_kws\": {\"markersize\": 15}},\n", ")\n", "\n", "# --- Rendering ---\n", "# Draw marquee elements.\n", "outset_grid.marqueeplot()\n", "\n", "# --- Annotations ---\n", "# Label plot elements.\n", "\n", "# Annotate a callout mark.\n", "outset_grid.source_axes.annotate(\n", " \"callout mark\",\n", " xy=(3, 4.5),\n", " xytext=(3.5, 3.5),\n", " horizontalalignment=\"left\",\n", " arrowprops=dict(arrowstyle=\"->\", lw=1),\n", ")\n", "\n", "# Annotate a callout leader.\n", "outset_grid.source_axes.annotate(\n", " \"callout leader\",\n", " xy=(2.5, 3.5),\n", " xytext=(3.5, 2.5),\n", " horizontalalignment=\"left\",\n", " arrowprops=dict(arrowstyle=\"->\", lw=1),\n", ")\n", "\n", "# Annotate the frame.\n", "outset_grid.source_axes.annotate(\n", " \"frame\",\n", " xy=(1, 2),\n", " xytext=(1, 2),\n", " horizontalalignment=\"center\",\n", ")\n", "\n", "# Annotate the marquee.\n", "outset_grid.source_axes.annotate(\n", " \"marquee\",\n", " xy=(1.5, 6),\n", " xytext=(1.5, 6.5),\n", " ha=\"center\",\n", " va=\"bottom\",\n", " arrowprops=dict(\n", " arrowstyle=\"-[, widthB=4.0, lengthB=1.0\", lw=2.0, color=\"k\"\n", " ),\n", ")\n", "\n", "# --- Finalization ---\n", "# Customize titles for the source and outset axes.\n", "outset_grid.broadcast_source(lambda ax: ax.set_title(\"source axes\"))\n", "outset_grid.broadcast_outset(lambda ax: ax.set_title(\"outset axes\"))\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The outset library creates multi-scale data visualizations that composite a \"**source**\" plot with magnified subsection views.\n", "These excerpted plot elements are termed as \"**outsets**.\"\n", "\n", "The zoom relationships between a source plot and accompanying outsets is indicated by \"**marquee**\" frame annotations.\n", "A marquee is composed of ***(1)*** *a bounding frame* and ***(2)*** *a callout*, containing ***(i)*** *an angled leader* that tapers to ***(ii)*** an *identifying mark*.\n", "\n", "The figure-level `outset.OutsetGrid` wrangles orchesration of these figure elements over an axis grid, composed of the source plot beside outset magnified panels.\n", "Magnified regions, as well as figure styling and layout, are specified via the `OutsetGrid` initializer.\n", "The `OutsetGrid.map_dataframe` and `OutsetGrid.broadcast` methods facilitate population of axes grid content.\n", "The `OutsetGrid.marqueeplot` method, usually called last, dispatches rendering of marquee annotations.\n", "The next sections will cover how to initialize and populate `OutsetGrid` plots in detail." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ... continue working on outset_grid object from previous frame\n", "\n", "# --- Inset and Outset Creation ---\n", "# Rearrange outset axes as insets onto the source plots\n", "otst.inset_outsets(outset_grid, insets=\"NW\")\n", "\n", "# --- Additional Annotations ---\n", "# Further annotate to label the inset and outset axes.\n", "\n", "# Annotate the inset and outset axes with an arrow pointing to them.\n", "outset_grid.source_axes.annotate(\n", " \"inset\\noutset\\naxes\",\n", " xy=(3.5, 8.7),\n", " xytext=(3.9, 8.7),\n", " clip_on=False,\n", " ha=\"left\",\n", " va=\"center\",\n", " arrowprops=dict(\n", " arrowstyle=\"-[, widthB=1.5, lengthB=0.5\", lw=2.0, color=\"k\"\n", " ),\n", ")\n", "\n", "# Add an arrow pointing upwards without text to indicate a specific area.\n", "outset_grid.source_axes.annotate(\n", " \"\",\n", " xy=(2.5, 10.2),\n", " xytext=(2.5, 10.21),\n", " clip_on=False,\n", " ha=\"center\",\n", " va=\"bottom\",\n", " arrowprops=dict(arrowstyle=\"-[, widthB=10, lengthB=0.5\", lw=2.0, color=\"k\"),\n", " annotation_clip=False,\n", ")\n", "\n", "# --- Display ---\n", "# Render and display the figure with all the elements.\n", "# Must call explicitly to draw a second time after render in previous frame.\n", "display(outset_grid.figure)\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If desired, outset axes can be transformed into *insets* over the source axes by running an `OutsetGrid` through the `outset.inset_outsets` function." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Data Preparation ---\n", "# Create a DataFrame with sample data defining the extent of marquee elements\n", "data = pd.DataFrame(\n", " {\n", " \"x\": [0, 0.2, 2.3, 1.8, 0.5, 2, 5.8, 5.87, 5.95, 6, 6],\n", " \"y\": [1, 2, 2.9, 3, 1.5, 4, 8, 8.2, 8.6, 9, 10],\n", " \"outset\": [\n", " \"group1\",\n", " \"group1\",\n", " \"group1\",\n", " \"group1\",\n", " \"group1\",\n", " \"group1\",\n", " \"group2\",\n", " \"group2\",\n", " \"group2\",\n", " \"group2\",\n", " \"group2\",\n", " ],\n", " }\n", ")\n", "\n", "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object for creating plots with grouped data.\n", "outset_grid = otst.OutsetGrid(\n", " data=data,\n", " x=\"x\",\n", " y=\"y\",\n", " col=\"outset\",\n", " hue=\"outset\",\n", " marqueeplot_kws={\"mark_glyph_kws\": {\"markersize\": 15}},\n", ")\n", "\n", "# --- Mapping Data ---\n", "# Map filled then outline KDE plot (filled) across all axes\n", "outset_grid.map_dataframe(\n", " sns.kdeplot,\n", " x=\"x\",\n", " y=\"y\",\n", " alpha=0.2,\n", " levels=2,\n", " legend=False,\n", " fill=True,\n", " thresh=0.01,\n", ")\n", "outset_grid.map_dataframe(\n", " sns.kdeplot,\n", " x=\"x\",\n", " y=\"y\",\n", " levels=2,\n", " legend=False,\n", " thresh=0.01,\n", ")\n", "\n", "# Scatter data on all axes.\n", "outset_grid.map_dataframe(sns.scatterplot, x=\"x\", y=\"y\", legend=False)\n", "\n", "# --- Finalization ---\n", "# Set titles for the source and outset axes.\n", "outset_grid.broadcast_source(lambda ax: ax.set_title(\"source axes\"))\n", "outset_grid.broadcast_outset(lambda ax: ax.set_title(\"outset axes\"))\n", "\n", "# Add a legend and fix any introduced skew\n", "outset_grid.add_legend()\n", "outset_grid.equalize_aspect()\n", "\n", "# --- Annotations ---\n", "# Annotate to indicate a data group on the plot.\n", "outset_grid.source_axes.annotate(\n", " \"data group\",\n", " xy=(1, 6),\n", " xytext=(1, 6.5),\n", " ha=\"center\",\n", " va=\"bottom\",\n", " arrowprops=dict(\n", " arrowstyle=\"-[, widthB=5.0, lengthB=1.0\", lw=2.0, color=\"k\"\n", " ),\n", ")\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Marquee annotations are optional, and can eschewed by simply not calling `OutsetGrid.marqueeplot`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Inferred Zoom Areas" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `outset.OutsetGrid` class closely tracks the design and API conventions of `seaborn.FacetGrid`.\n", "You can think of an `OutsetGrid` as a `FacetGrid` with an unfaceted (i.e., all data) plot tacked on the front and some additional mechanisms for control of axes data limits." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object for creating a series of scatter plots.\n", "# The plots will be based on the 'table' and 'depth' features, categorized by\n", "# the 'cut' of the diamonds.\n", "og = otst.OutsetGrid(\n", " data=sns.load_dataset(\"diamonds\").dropna(),\n", " aspect=1.5, # Set aspect ratio\n", " x=\"table\",\n", " y=\"depth\",\n", " col=\"cut\",\n", " col_order=[\"Premium\"], # Focus on 'Premium' cut diamonds only\n", " marqueeplot_kws={\n", " \"mark_glyph\": otst_mark.MarkRomanBadges, # Use Roman badge markers\n", " \"leader_stretch\": 0.2,\n", " \"leader_stretch_unit\": \"inches\",\n", " \"leader_tweak\": otst_tweak.TweakReflect(horizontal=True),\n", " },\n", ")\n", "\n", "# --- Draw Data ---\n", "# Map scatter plot over all axes.\n", "og.map_dataframe(\n", " otst_patched.scatterplot,\n", " x=\"table\",\n", " y=\"depth\",\n", " legend=False, # No individual legend for plots\n", " zorder=-2,\n", " color=\"gray\",\n", " s=otst_util.SplitKwarg( # pass different args to source vs. outset axes\n", " source=10, # Size for source markers\n", " outset=10, # Size for outset markers\n", " ),\n", ")\n", "\n", "# --- Finalization ---\n", "# Set a title and add a legend\n", "og.source_axes.set_title(\"cut = All\")\n", "\n", "# Render marquee elements\n", "og.marqueeplot()\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Here, `OutsetGrid(col=\"cut\", col_order=[\"Premium\"])` have been specified.\n", "This indicates that the dataframe subset where `df[\"cut\"] == \"Premium\"` should be plotted as an outset.\n", "Then, `OutsetGrid.map_dataframe(sns.scatterplot)` is called to render data --- all data on the source plot and \"Premium\" cut data on the outset.\n", "Finally, `OutsetGrid.marqueeplot` adds marquee annotations and finalizes axes data limits.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object to create plots with grouped data.\n", "# The plots will be based on 'table' and 'depth' features, categorized by 'cut'.\n", "outset_grid = otst.OutsetGrid(\n", " data=sns.load_dataset(\"diamonds\").dropna(),\n", " aspect=1.5,\n", " x=\"table\",\n", " y=\"depth\",\n", " col=\"cut\",\n", " col_order=[\"Fair\", \"Premium\"], # Focus on 'Fair' and 'Premium' cuts only.\n", " hue=\"cut\",\n", " hue_order=[\"Fair\", \"Premium\"],\n", " marqueeplot_kws={\n", " \"mark_glyph\": otst_mark.MarkRomanBadges, # Roman numeral callout IDs\n", " \"leader_stretch\": 0.2,\n", " \"leader_stretch_unit\": \"inches\",\n", " \"leader_tweak\": otst_tweak.TweakReflect(vertical=True),\n", " },\n", ")\n", "\n", "# --- Mapping Plotter across Faceted Data ---\n", "# Map a scatter plot over all axes to visualize the diamond 'table' vs 'depth'.\n", "outset_grid.map_dataframe(\n", " otst_patched.scatterplot,\n", " x=\"table\",\n", " y=\"depth\",\n", " legend=False, # Disable individual legend for plots.\n", " zorder=-2, # Ensure the scatter plot is behind other plot elements.\n", " s=30, # Set marker size.\n", ")\n", "\n", "# --- Finalization ---\n", "# Draw marquee elements.\n", "outset_grid.marqueeplot()\n", "\n", "# Add a legend to the plot to make it informative.\n", "outset_grid.add_legend()\n", "\n", "# Equalize the aspect ratio of the plots to correct any skew introduced.\n", "outset_grid.equalize_aspect()\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Passing the same key `OutsetGrid(col=\"cut\", hue=\"cut\")` will cause marquee annotations in each axes to render in successive palette colors.\n", "Note that hue mapping can be explicitly suppressed by passing `hue=False`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Data Preparation ---\n", "# Create a DataFrame with sample data defining the extent of marquee elements\n", "data = sns.load_dataset(\"diamonds\").dropna()\n", "filtered_data = data[\n", " (data[\"color\"] == \"E\") & (data[\"cut\"].isin([\"Fair\", \"Very Good\"]))\n", "]\n", "\n", "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object to create plots with grouped data.\n", "# The plots will be based on 'carat' and 'price' features, categorized by 'cut'\n", "# and 'clarity'.\n", "outset_grid = otst.OutsetGrid(\n", " data=filtered_data,\n", " aspect=1.5,\n", " x=\"carat\",\n", " y=\"price\",\n", " col=\"cut\",\n", " col_order=[\"Fair\", \"Very Good\"],\n", " hue=\"clarity\",\n", " hue_order=[\"I1\", \"VVS2\"],\n", " marqueeplot_kws={\n", " \"mark_glyph\": otst.mark.MarkRomanBadges, # roman numeral identifiers\n", " \"leader_stretch\": 0.2,\n", " \"leader_stretch_unit\": \"inches\",\n", " \"leader_tweak\": otst.tweak.TweakReflect(vertical=True),\n", " },\n", ")\n", "\n", "# --- Mapping Plotter across Faceted Data ---\n", "# Map a scatter plot over all axes to visualize the diamond 'carat' vs 'price'.\n", "outset_grid.map_dataframe(\n", " otst.patched.scatterplot, # patch is for mwaskom/seaborn issues #3601\n", " x=\"carat\",\n", " y=\"price\",\n", " legend=False,\n", " zorder=-2,\n", " s=30, # marker size\n", ")\n", "\n", "# --- Finalization ---\n", "# Draw marquee elements.\n", "outset_grid.marqueeplot()\n", "\n", "# Add a legend to the plot to make it informative.\n", "outset_grid.add_legend()\n", "\n", "# Equalize the aspect ratio of the plots to correct any skew introduced.\n", "outset_grid.equalize_aspect()\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Passing different keys `OutsetGrid(col=\"cut\", hue=\"clarity\")` will facet on both keys, rendering several hue-mapped marquees per outset frame." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Explicit Zoom Areas" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# adapted from https://seaborn.pydata.org/examples/wide_data_lineplot.html\n", "\n", "# --- Data Preparation ---\n", "# Create a random state and generate a DataFrame with cumulative sum values.\n", "values = np.random.RandomState(365).randn(365, 4).cumsum(axis=0)\n", "dates = np.array(range(365))\n", "data = pd.DataFrame(values, dates, columns=[\"A\", \"B\", \"C\", \"D\"])\n", "data = data.rolling(7).mean() # Apply a rolling mean with a 7-day window.\n", "\n", "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object with explicit marquee position\n", "outset_grid = otst.OutsetGrid(\n", " aspect=2,\n", " data=[(210, 6, 250, 12)], # Explicit marquee positioning.\n", " col_wrap=1, # Ensure vertical layout (two rows)\n", " x=\"days\",\n", " y=\"profit\",\n", ")\n", "\n", "# --- Broadcast Plotter ---\n", "# Broadcast a line plot over all axes..\n", "outset_grid.broadcast(\n", " sns.lineplot,\n", " data=data,\n", " linewidth=2.5,\n", " zorder=-1, # Ensure the line plot is behind other plot elements.\n", ")\n", "\n", "# --- Finalization ---\n", "# Draw marquee elements and conclude the script.\n", "outset_grid.marqueeplot()\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To manually specify zoom frames instead of inferring them from data structure, pass frame coordinates instead of a dataframe when initializing `OutsetGrid`.\n", "In this case, `outset.OutsetGrid(data=[(210, 6, 250, 12)])`.\n", "\n", "Because data is not faceted over plot components, we will need to provide data when rendering plot elements.\n", "To manually pass a `data` kwarg through to the plotting function, use `OutsetGrid.broadcast` instead of `OutsetGrid.map_dataframe`.\n", "In this case, `OutsetGrid.broadcast(sns.lineplot, data=df)`.\n", "The plotter will be called with the same data on each axes, then the subsequent call to `marqueeplot` will adjust axes data limits to magnify areas specified at `OutsetGrid` initialization in outset axes." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# adapted from https://seaborn.pydata.org/examples/wide_data_lineplot.html\n", "\n", "# --- Data Preparation ---\n", "# Create a random state and generate a DataFrame with cumulative sum values.\n", "values = np.random.RandomState(365).randn(365, 4).cumsum(axis=0)\n", "dates = np.array(range(365))\n", "data = pd.DataFrame(values, dates, columns=[\"A\", \"B\", \"C\", \"D\"])\n", "data = data.rolling(7).mean() # Apply a rolling mean with a 7-day window.\n", "\n", "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object to create line and scatter plots.\n", "outset_grid = otst.OutsetGrid(\n", " aspect=2,\n", " data=[(210, 6, 250, 12)], # explicit marquee positioning\n", " col_wrap=1, # Ensure vertical layout (two rows)\n", " x=\"days\",\n", " y=\"profit\",\n", ")\n", "\n", "# --- Broadcast Plotter ---\n", "# Broadcast a line plot over all axes .\n", "outset_grid.broadcast(\n", " sns.lineplot,\n", " data=data,\n", " linewidth=otst.util.SplitKwarg(source=2.5, outset=10), # Line width.\n", " zorder=-1, # Ensure the line plot is behind other plot elements.\n", ")\n", "\n", "# Broadcast a scatter plot to highlight specific data points.\n", "outset_grid.broadcast_source(\n", " sns.scatterplot,\n", " data=data[::20], # Data points to highlight.\n", " zorder=-1, # Ensure the scatter plot is behind other plot elements.\n", " s=50, # Marker size.\n", ")\n", "\n", "# --- Finalization ---\n", "# Apply additional plot settings and draw marquee elements.\n", "outset_grid.broadcast_outset(lambda: plt.axis(\"off\")) # Turn off axis.\n", "outset_grid.marqueeplot() # Draw marquee elements.\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Source axes and outset axes can be targeted in isolation using `OutsetGrid.broadcast_source` and `OutsetGrid.broadcast_outset` methods.\n", "These work equivalently to `OutsetGrid.broadcast`, except only dispatch the plotter to the source or outset axes, respectively.\n", "For scenarios where an individual kwarg's value should differ between source and outset axes, `OutsetGrid.broadcast` may be used in conjuction with the `outset.util.SplitKwarg` utility. In the example above, for instance, `SplitKwarg` is used to differentiate line width between source and outset axes, `OutsetGrid.broadcast(sns.lineplot, linewidth=otst.util.SplitKwarg(source=2.5, outset=10))`.\n", "\n", "(Equivalent `OutsetGrid.map_dataframe_source` and `OutsetGrid.map_dataframe_outset` methods are also available for the data-driven `OutsetGrid` interface, and `OutsetGrid.map_dataframe` is compatible with `SplitKwarg`.)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Aspect Ratio" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As much fun as we might have with narrow and/or wide Grace Hopper..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Data Preparation ---\n", "# Load the sample image of Grace Hopper.\n", "with mpl_cbook.get_sample_data(\"grace_hopper.jpg\") as image_file:\n", " image = plt.imread(image_file)\n", "\n", "# --- Plot Initialization ---\n", "# Initialize a subplot with 1 row and 2 columns.\n", "fig, axs = plt.subplots(1, 2)\n", "\n", "# --- Image Display with Bad Aspects ---\n", "# Display the image with an exaggerated aspect ratio.\n", "axs[0].imshow(image, aspect=7, extent=(0, 1, 0, 1), origin=\"upper\", zorder=-1)\n", "axs[0].set_axis_off() # Remove the axis for a cleaner look.\n", "\n", "# Display the image with a compressed aspect ratio.\n", "axs[1].imshow(image, aspect=0.3, extent=(0, 1, 0, 1), origin=\"upper\", zorder=-1)\n", "axs[1].set_axis_off() # Remove the axis for a cleaner look.\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "... usually we want to make sure images keep their natural aspect ratio.\n", "Luckily, `OutsetGrid` can take care of that for us.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# adapted from https://seaborn.pydata.org/examples/scatter_bubbles.html\n", "\n", "# --- Data Preparation ---\n", "# Load the 'mpg' dataset and remove any missing values.\n", "data = sns.load_dataset(\"mpg\").dropna()\n", "\n", "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object to create a scatter plot with bubble sizes.\n", "outset_grid = otst.OutsetGrid(\n", " data=[(73.5, 23.5, 78.5, 31.5)], # Explicit marquee positioning.\n", " color=sns.color_palette()[-1], # Set color for the marquee.\n", " marqueeplot_kws={\n", " \"mark_glyph\": otst.mark.MarkAlphabeticalBadges(\n", " start=\"A\", # Uppercase alphabetical identifiers.\n", " ),\n", " \"frame_outer_pad\": 0.2,\n", " \"frame_outer_pad_unit\": \"inches\",\n", " \"frame_face_kws\": {\"facecolor\": \"none\"}, # Transparent frame.\n", " },\n", ")\n", "\n", "# --- Broadcast Plotter ---\n", "# Broadcast a scatter plot to visualize 'horsepower' vs 'mpg' with bubble sizes representing 'weight'.\n", "outset_grid.broadcast(\n", " sns.scatterplot,\n", " data=data,\n", " x=\"horsepower\",\n", " y=\"mpg\",\n", " hue=\"origin\", # Color by 'origin'.\n", " size=\"weight\", # Size bubbles by 'weight'.\n", " sizes=(40, 400), # Range of bubble sizes.\n", " alpha=0.5, # Transparency of bubbles.\n", " palette=\"muted\", # Color palette.\n", " zorder=0, # Z-order for layering.\n", ")\n", "\n", "# --- Finalization ---\n", "# Draw marquee elements and set plot titles.\n", "outset_grid.marqueeplot(equalize_aspect=False)\n", "outset_grid.add_legend()\n", "outset_grid.set_titles(\"\")\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Because synchronized aspect ratios benefit intuitive comparison between outsets and the source plot, under default settings `OutsetGrid` operations automatically apply corrections to provide equivalent aspect ratios across the source and outset plots.\n", "Correction is performed for all plot types, not just images.\n", "\n", "The `OutsetGrid` map dataframe, broadcast, and insetting methods equalize aspect ratio among axes, if necessary, before returning.\n", "If you perform other operations that might distort aspect ratios (e.g., adding a legend, stripping axes ticks, etc.) you should call `OutsetGrid.equalize_aspect` directly at the end of plotting to re-sync aspect ratios\n", "\n", "In cases where mismatching aspect ratios are desired --- for example, to allow for \"better\" zoom when aspect is narrow --- use the `OutsetGrid.marqueeplot(equalize_aspect=False)` kwarg." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Data Preparation ---\n", "# Load the sample image of Grace Hopper.\n", "with mpl_cbook.get_sample_data(\"grace_hopper.jpg\") as image_file:\n", " image = plt.imread(image_file)\n", "\n", "h, w = image.shape[0], image.shape[1]\n", "\n", "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object for displaying the image with specific marquee frames.\n", "outset_grid = otst.OutsetGrid(\n", " data=otst.util.NamedFrames(\n", " {\n", " # Coordinates for the 'hat' marquee.\n", " \"hat\": (0.42 * w, 0.78 * h, 0.62 * w, 0.98 * h),\n", " # Coordinates for the 'badge' marquee.\n", " \"badge\": (0.10 * w, 0.14 * h, 0.40 * w, 0.21 * h),\n", " }\n", " ),\n", " col=\"swag\", # Name for column mapping (used in titles)\n", " hue=\"swag\", # Name for Hue mapping (used in legends)\n", " aspect=w / h, # match image aspect\n", ")\n", "\n", "# --- Image Display ---\n", "# Turn off the axis and display the image with specified z-order and origin.\n", "outset_grid.broadcast(lambda: plt.axis(\"off\"))\n", "outset_grid.broadcast(\n", " plt.imshow, image, extent=(0, w, 0, h), origin=\"upper\", zorder=-1\n", ")\n", "\n", "# --- Finalization ---\n", "# Draw marquee elements and set the title for the source axes.\n", "outset_grid.source_axes.set_title(\"The Hopster\", loc=\"left\")\n", "outset_grid.marqueeplot(preserve_aspect=True)\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Default aspect ratio synchronization among axes will ensure aspect ratios match, but original aspect ratios may not be preserved under map, broadcast, and insetting methods.\n", "To keep a specific aspect ratio, like you might want when working with an image, pass the `OutsetGrid(preserve_aspect=True)` kwarg." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Layout Control" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object to create KDE plots for the 'iris' dataset.\n", "outset_grid = otst.OutsetGrid(\n", " data=sns.load_dataset(\"iris\").dropna(),\n", " x=\"petal_width\",\n", " y=\"petal_length\",\n", " col=\"species\", # Separate plots by species.\n", " col_wrap=2, # Display plots in 2 columns, multiple rows\n", " color=sns.color_palette()[1], # Set color for the marquee.\n", " marqueeplot_kws={\n", " \"mark_glyph\": otst.mark.MarkAlphabeticalBadges,\n", " }, # Alphabetical identifiers, lowercase,\n", " marqueeplot_source_kws={ # Reorient marquee annotations on source axes\n", " \"leader_tweak\": otst.tweak.TweakReflect(),\n", " },\n", " marqueeplot_outset_kws={ # Reorient marquee annotations on outset axes\n", " \"leader_tweak\": otst.tweak.TweakReflect(vertical=True),\n", " },\n", ")\n", "\n", "# --- Map KDE Plot over faceted data ---\n", "# Map KDE plots separately for source and outset data.\n", "outset_grid.map_dataframe_source(\n", " sns.kdeplot, x=\"petal_width\", y=\"petal_length\", legend=False, zorder=0\n", ")\n", "outset_grid.map_dataframe_outset(\n", " sns.kdeplot,\n", " x=\"petal_width\",\n", " y=\"petal_length\",\n", " fill=True,\n", " legend=False,\n", " zorder=0,\n", ")\n", "\n", "# --- Finalization ---\n", "# Draw marquee elements.\n", "outset_grid.marqueeplot()\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Marquee styling is controlled by passing content via the `OutsetGrid.__init__(marqueeplot_kws=dict())` kwarg.\n", "\n", "Notable options include `marqueeplot_kws=dict(mark_glyph=outset.mark.MarkAlphabeticalBadges)` to switch from numerical marquee identifiers to alphabetical identifiers.\n", "Use `marqueeplot_kws=dict(leader_tweak=outset.tweak.TweakReflect(horizontal=True))` to flip the orientation of marque identifiers.\n", "(The `TweakReflect` functor also accepts a `vertical` kwarg.)\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object to create scatter plots for the 'penguins' dataset.\n", "outset_grid = otst.OutsetGrid(\n", " data=sns.load_dataset(\"penguins\").dropna(),\n", " x=\"bill_length_mm\",\n", " y=\"bill_depth_mm\",\n", " col=\"island\", # Separate plots by island.\n", " col_order=[\"Biscoe\", \"Dream\"], # Order of islands to display.\n", " hue=\"species\", # Color by species.\n", " marqueeplot_kws={\n", " \"mark_glyph\": otst.mark.MarkRomanBadges\n", " }, # Roman numeral identifiers.\n", " marqueeplot_source_kws={\n", " \"leader_face_kws\": {\"alpha\": 0.2},\n", " # Push marquee identifiers apart from crowded area\n", " \"leader_tweak\": otst.tweak.TweakSpreadArea(\n", " spread_factor=(2.6, 2.5),\n", " xlim=(45.5, 52),\n", " ylim=(21, 24),\n", " ),\n", " },\n", ")\n", "\n", "# --- Map Scatter Plot over faceted data ---\n", "# Map scatter plots for 'bill_length_mm' vs 'bill_depth_mm'.\n", "outset_grid.map_dataframe(\n", " sns.scatterplot, x=\"bill_length_mm\", y=\"bill_depth_mm\", legend=False\n", ")\n", "\n", "# --- Finalization ---\n", "# Draw marquee elements, set axis labels, and add a legend.\n", "outset_grid.marqueeplot()\n", "outset_grid.set_axis_labels(\"bill length (mm)\", \"bill depth (mm)\")\n", "outset_grid.add_legend()\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `TweakSpreadArea` functor is available to resolve collisions between marquee identifying marks by spreading marks within a designated area apart.\n", "In this case,\n", "\n", "```python\n", "OutsetGrid(\n", " marqueeplot_source_kws={\n", " \"leader_tweak\": otst.tweak.TweakSpreadArea(\n", " spread_factor=(2.6, 2.5),\n", " xlim=(45.5, 52),\n", " ylim=(21, 24),\n", " ),\n", " },\n", ")\n", "```\n", "pushes the *i* and *iii* markers apart.\n", "\n", "See documentation for `outset.marqueeplot`, `outset.draw_marquee`, or the gallery page to see additional available styling and layout options." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Plot Initialization ---\n", "# Initialize an OutsetGrid object to create KDE plots for the 'iris' dataset.\n", "outset_grid = otst.OutsetGrid(\n", " data=sns.load_dataset(\"iris\").dropna(),\n", " x=\"petal_width\",\n", " y=\"petal_length\",\n", " col=\"species\", # Separate plots by species.\n", " color=sns.color_palette()[1], # Set color for the marquee.\n", " marqueeplot_kws={\n", " \"mark_glyph\": otst.mark.MarkAlphabeticalBadges,\n", " }, # Alphabetical identifiers, lowercase,\n", " marqueeplot_source_kws={ # Reorient marquee annotations on source axes\n", " \"leader_tweak\": otst.tweak.TweakReflect(),\n", " },\n", " marqueeplot_outset_kws={ # Reorient marquee annotations on outset axes\n", " \"leader_tweak\": otst.tweak.TweakReflect(vertical=True),\n", " },\n", ")\n", "\n", "# --- Map KDE Plot over faceted data ---\n", "# Map KDE plots separately for source and outset data.\n", "outset_grid.map_dataframe_source(\n", " sns.kdeplot, x=\"petal_width\", y=\"petal_length\", legend=False, zorder=0\n", ")\n", "outset_grid.map_dataframe_outset(\n", " sns.kdeplot,\n", " x=\"petal_width\",\n", " y=\"petal_length\",\n", " fill=True,\n", " legend=False,\n", " zorder=0,\n", ")\n", "\n", "# --- Finalization ---\n", "otst.inset_outsets(outset_grid, \"SE\") # move outsets into lower right of source\n", "outset_grid.marqueeplot() # Draw marquee elements.\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Any `OutsetGrid` can be transformed into an inset plot through `outset.inset_outsets`.\n", "This function takes an `OutsetGrid` as an argument, and rearranges it to reposition outset axes directly over the source plot axes.\n", "The corner to place outset axes in can be designated as `NE` (upper right), `SE` (lower right), etc., like `outset.inset_outsets(outset_grid, \"SE\")`.\n", "\n", "Several levels of abstraction are available to decide inset geometry --- a call to `outset.util.layout_corner_insets` or even hard axes-relative coordinates can be provided in place of a cardinal corner designation.\n", "See the gallery for examples." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Axes-level Interfaces" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Most use cases should only need `OutsetGrid`, but outset does make more granular control available through lower-level APIs." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# --- Plot Initialization ---\n", "# Initialize a subplot.\n", "fig, ax = plt.subplots(1)\n", "\n", "# --- Marquee Plotting ---\n", "# Create a marquee plot with the 'iris' dataset.\n", "otst.marqueeplot(\n", " data=sns.load_dataset(\"iris\"),\n", " x=\"petal_length\",\n", " y=\"petal_width\",\n", " hue=\"species\", # Color by species.\n", " ax=ax, # Specify the axis for plotting.\n", " leader_stretch=0.4, # Stretch factor for leaders.\n", " leader_tweak=otst.tweak.TweakReflect(\n", " vertical=True\n", " ), # Tweak leaders for better visibility.\n", ")\n", "\n", "# --- Scatter Plot ---\n", "# Overlay a scatter plot on the same axes.\n", "sns.scatterplot(\n", " data=sns.load_dataset(\"iris\"),\n", " x=\"petal_length\",\n", " y=\"petal_width\",\n", " hue=\"species\", # Color by species.\n", " ax=ax, # Specify the axis for plotting.\n", ")\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Use `marqueeplot` for seaborn-like, data-oriented application of marquee annotations to a single `Axes`.\n", "(This function determines frame positions according to the x/y extents of data subsets.)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# adapted from https://seaborn.pydata.org/examples/smooth_bivariate_kde.html\n", "\n", "# --- Plot Initialization ---\n", "# Initialize a subplot.\n", "fig, ax = plt.subplots(1)\n", "\n", "# --- KDE Plotting ---\n", "# Create a KDE plot of body mass vs. bill depth for the penguins dataset.\n", "sns.kdeplot(\n", " data=sns.load_dataset(\"penguins\"),\n", " x=\"body_mass_g\",\n", " y=\"bill_depth_mm\",\n", " ax=ax,\n", " fill=True,\n", " clip=((2200, 6800), (10, 25)), # Limit the data to a specific range.\n", " thresh=0,\n", " levels=100, # Set the number of contour levels.\n", " cmap=\"rocket\", # Color map.\n", ")\n", "\n", "# --- Marquee Annotation ---\n", "# Draw a single marquee around a specific region of interest.\n", "otst.draw_marquee(\n", " (5200, 6000), # X range for the marquee.\n", " (14, 16), # Y range for the marquee.\n", " ax,\n", " color=\"teal\", # Color of the marquee.\n", " mark_glyph=otst.mark.MarkArrow(rotate_angle=-45), # Marquee identifier\n", " leader_stretch_unit=\"inches\",\n", " leader_stretch=0.2, # Stretch factor for leaders.\n", " zorder=10,\n", ")\n", "\n", "# --- Annotation ---\n", "# Add a fun text annotation.\n", "ax.annotate(\"zoink?\", (6300, 17), color=\"teal\", zorder=5)\n", "\n", "pass # sponge up last return value, if any" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Use `draw_marquee` to manually add individual annotations." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Further Subjects\n", "\n", "The class `OutsetGrid` inherits from `seaborn.FacetGrid`, which provides a rich set of initializer kwargs and member properties/functions available to the end user.\n", "See the `README` and examples in the gallery for examples demonstrating these customization capabilities, as well as the full range of styling customization capabilities available.\n", "\n", "Some caveats and known limitations should be noted.\n", "\n", "- Try to call `OutsetGrid.marqueeplot` later rather than earlier --- the layout algorithms make use of information about current axes state to determine positioning and sizing of annotation elements.\n", "- `OutsetGrid` construction can sometimes be sensitive to operation order, especially when generating colorbars and legends.\n", " Again, refer to gallery for examples with working operation orders.\n", "- Inverted axes are currently not supported.\n", "- See [here](https://stackoverflow.com/a/77711930/17332200) for an example involving log-scale axes.\n", "\n", "Finally, if this code is useful to you, please consider leaving a [⭐ on GitHub](https://github.com/mmore500/outset/stargazers).\n", "Thanks!" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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" } }, "nbformat": 4, "nbformat_minor": 2 }