Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions docs/comparison.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
grplot vs. Matplotlib/Seaborn
=============================

Rather than assert that grplot reduces boilerplate, this page shows the same
dashboard implemented both ways so you can judge the difference directly.

Six-panel statistical dashboard
--------------------------------

The dashboard below combines six different chart families — a histogram
with KDE and summary statistics, an ECDF, a treemap, a pie chart, a Pareto
chart, and an annotated box + strip plot — each requiring its own
statistical-annotation logic (quantile lines and a stats box, a twin-axis
cumulative curve, confidence-interval whiskers).

Using grplot
~~~~~~~~~~~~

.. code-block:: python

from grplot import plot2d
import grplot_seaborn as gs

gs.set_theme(context='notebook', style='darkgrid', palette='deep')

tips = gs.load_dataset('tips')
ax = plot2d(plot={'[1,1]': 'histplot', '[1,2]': 'ecdfplot',
'[2,1]': 'treemapsplot', '[2,2]': 'pieplot',
'[3,1]': 'paretoplot', '[3,2]': 'boxplot+stripplot'},
Nx=2, Ny=3, df=tips, filter=(tips['total_bill'] > 10),
figsize=[18, 16], fontsize=12, legend_fontsize=11,
sep={'total_bill': '.c',
'.': ['Count', 'Proportion', '[2,1]', '[2,2]', 'Cumulative Percentage']},
statdesc={'[1,1]': {'total_bill': 'general'},
'[3,2]': {'total_bill': 'boxplot'}},
text={'Count': 'h', True: ['[2,1]', '[2,2]'], '[3,1]': {'total_bill': 'h+i'}},
tick_add={'total_bill': 'Rp(_)'},
title={'[1,1]': 'Histogram Count vs total_bill',
'[1,2]': 'ECDF Proportion vs total_bill',
'[2,1]': 'Treemaps of day',
'[2,2]': 'Pie of day',
'[3,1]': 'Pareto total_bill vs day',
'[3,2]': 'Box day vs total_bill'},
alpha={'[1,1]': 0.75, '[3,1]': 0.75},
kde=True)

**32 lines.**

.. image:: _static/plots/dashboard_2d.png
:alt: Six-panel statistical dashboard generated by a single plot2d call
:align: center

Using Matplotlib/Seaborn
~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: python

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import seaborn as sns
import squarify # pip install squarify (for the treemap)

sns.set_theme(context='notebook', style='darkgrid', palette='deep')
tips = sns.load_dataset('tips')
df = tips[tips['total_bill'] > 10].copy()
fig, axes = plt.subplots(3, 2, figsize=(18, 16))
rp = lambda v: f"Rp{v:,.2f}".replace(",", ".")

# Histogram + KDE
ax = axes[0, 0]
tb = df['total_bill']
counts, bins, patches = ax.hist(tb, bins=10, alpha=0.75, edgecolor='white')
for c, left, right in zip(counts, bins[:-1], bins[1:]):
if c > 0:
ax.text((left + right) / 2, c + 0.5, f"{int(c)}", ha='center', fontsize=12)
ax2 = ax.twinx()
sns.kdeplot(tb, ax=ax2, color='steelblue')
ax2.set_yticks([])
stats = {
'count': len(tb), 'non zero count': (tb != 0).sum(), 'unique count': tb.nunique(),
'std': tb.std(), 'range': tb.max() - tb.min(), 'min': tb.min(),
'q1': tb.quantile(0.25), 'median': tb.median(), 'mean': tb.mean(),
'q3': tb.quantile(0.75), 'max': tb.max(),
}
for key in ['min', 'q1', 'median', 'mean', 'q3', 'max']:
color = {'min': 'gray', 'q1': 'blue', 'median': 'purple',
'mean': 'orange', 'q3': 'green', 'max': 'red'}[key]
ls = '--' if key not in ('median', 'mean') else ':'
ax.axvline(stats[key], color=color, linestyle=ls, linewidth=1)
txt = "\n".join(
[f"count: {stats['count']}", f"non zero count: {stats['non zero count']}",
f"unique count: {stats['unique count']}", f"std: {rp(stats['std'])}",
f"range: {rp(stats['range'])}"] +
[f"{k}: {rp(stats[k])}" for k in ['min', 'q1', 'median', 'mean', 'q3', 'max']]
)
ax.text(0.98, 0.97, txt, transform=ax.transAxes, fontsize=11,
va='top', ha='right', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: rp(v)))
ax.set_title('Histogram Count vs total_bill')
ax.set_ylabel('Count')

# ECDF
ax = axes[0, 1]
sns.ecdfplot(tb, ax=ax)
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: rp(v)))
ax.set_title('ECDF Proportion vs total_bill')
ax.set_ylabel('Proportion')

# Treemap
ax = axes[1, 0]
day_counts = df['day'].value_counts()
colors = sns.color_palette('deep', len(day_counts))
squarify.plot(sizes=day_counts.values,
label=[f"{d}\n{c}" for d, c in day_counts.items()],
color=colors, ax=ax, edgecolor='white')
ax.set_title('Treemaps of day')
ax.axis('off')

# Pie
ax = axes[1, 1]
day_counts_pie = df['day'].value_counts()
ax.pie(day_counts_pie.values, labels=day_counts_pie.index,
autopct='%1.1f%%', colors=sns.color_palette('deep'))
ax.set_title('Pie of day')

# Pareto
ax = axes[2, 0]
means = df.groupby('day', observed=True)['total_bill'].mean().sort_values(ascending=False)
cum_pct = means.cumsum() / means.sum() * 100
ax.bar(means.index, means.values, alpha=0.75)
for i, v in enumerate(means.values):
ax.text(i, v / 2, rp(v), ha='center', fontsize=11)
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: rp(v)))
ax.set_ylabel('total_bill')
ax3 = ax.twinx()
ax3.plot(means.index, cum_pct.values, color='black', marker='D')
for i, v in enumerate(cum_pct.values):
ax3.text(i, v + 3, f"{v:.1f}%", ha='center', fontsize=11)
ax3.set_ylim(0, 110)
ax3.yaxis.set_major_formatter(mticker.PercentFormatter())
ax3.set_ylabel('Cumulative Percentage')
ax3.grid(False)
ax.set_title('Pareto total_bill vs day')

# Box + strip
ax = axes[2, 1]
order = ['Thur', 'Fri', 'Sat', 'Sun']
sns.boxplot(data=df, x='total_bill', y='day', order=order, ax=ax, orient='h')
sns.stripplot(data=df, x='total_bill', y='day', order=order, ax=ax,
color='black', size=3, alpha=0.6)
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: rp(v)))
ax.set_title('Box day vs total_bill')
q1, q3 = tb.quantile(0.25), tb.quantile(0.75)
iqr = q3 - q1
lw = tb[tb >= q1 - 1.5 * iqr].min()
uw = tb[tb <= q3 + 1.5 * iqr].max()
se = tb.std() / np.sqrt(len(tb))
box_stats = {
'min': tb.min(), 'lower whisker': lw, 'q1': q1,
'95% CI low': tb.mean() - 1.96 * se, 'median': tb.median(),
'mean': tb.mean(), '95% CI hi': tb.mean() + 1.96 * se,
'q3': q3, 'upper whisker': uw, 'max': tb.max(),
}
txt = "\n".join(f"{k}: {rp(v)}" for k, v in box_stats.items())
ax.text(1.02, 0.5, txt, transform=ax.transAxes, fontsize=10,
va='center', ha='left', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))

plt.tight_layout()
plt.savefig('dashboard.png', dpi=100, bbox_inches='tight')

**101 lines** — roughly 3x longer, because each panel's statistical
annotation (the stats box, the twin-axis cumulative line, the confidence
interval) is written out by hand instead of being generated from a
declarative argument.

.. image:: _static/plots/dashboard_matplotlib_tips.png
:alt: Equivalent six-panel dashboard built manually with Matplotlib and Seaborn
:align: center

Where the difference disappears
--------------------------------

This advantage is not universal, and it would be misleading to imply
otherwise. For a grid of *one repeated chart type* — nine box plots across a
metric/cluster grid, for example — grplot (37 lines) and a short Matplotlib
loop (34 lines) come out roughly the same length. grplot's boilerplate
reduction is concentrated in dashboards that mix several chart families, each
needing its own statistical-annotation code; it offers no particular
advantage when a simple loop over one chart type already does the job.

.. list-table::
:header-rows: 1
:widths: 60 15 20 15

* - Dashboard
- grplot
- Matplotlib/Seaborn
- Ratio
* - 6 heterogeneous panels (as above)
- 32 lines
- 101 lines
- 3.2×
* - 9 homogeneous panels (boxplot only, repeated across a metric/cluster grid)
- 37 lines
- 34 lines
- ~1.0×

In both cases, ``plot2d`` returns the underlying Matplotlib ``Axes`` array,
so any further per-panel customization is still available through the normal
Matplotlib API on top of the generated baseline.
Binary file added docs/dashboard_matplotlib_tips.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,31 @@ grplot

|pypi_ver| |license| |anaconda| |downloads| |coverage|

.. code-block:: python

from grplot import plot2d
import grplot_seaborn as gs

gs.set_theme(context='notebook', style='darkgrid', palette='deep')

tips = gs.load_dataset('tips')
ax = plot2d(plot={'[1,1]': 'histplot', '[1,2]': 'ecdfplot',
'[2,1]': 'treemapsplot', '[2,2]': 'pieplot',
'[3,1]': 'paretoplot', '[3,2]': 'boxplot+stripplot'},
Nx=2, Ny=3, df=tips, filter=(tips['total_bill'] > 10),
kde=True) # see Dashboard-Like for the full, annotated call

.. image:: _static/plots/dashboard_2d.png
:alt: Six-panel statistical dashboard generated by a single plot2d call
:align: center

*A histogram with KDE and summary statistics, an ECDF, a treemap, a pie
chart, a Pareto chart, and an annotated box + strip plot — six different
chart families, fully annotated, produced by one* ``plot2d`` *call. See*
:doc:`comparison` *for the equivalent Matplotlib/Seaborn code side by side,*
:doc:`plots/dashboard` *for this example's full write-up, and*
:doc:`plot_types` *for a worked example of every individual chart type.*

.. container:: feature-grid

.. hlist::
Expand Down Expand Up @@ -61,6 +86,7 @@ grplot
:hidden:

introduction
comparison
configuration
plot_types

Expand Down
11 changes: 10 additions & 1 deletion docs/introduction.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
Introduction
============

grplot introduces powerful features through an intuitive, hierarchical argument system. Every feature is directly accessible via ``plot2d`` parameters, allowing you to configure everything from high-level data inputs to the finest label details in a single function call.
grplot reduces the boilerplate of producing annotated, publication-ready
statistical figures: subplot layout, tick/number formatting, inset
statistical summaries, and value-label annotations are configured through
``plot2d`` parameters instead of separate Matplotlib/Seaborn calls per
figure. This page documents the hierarchical argument system and
configuration options that apply across *every* plot type. For a direct,
line-by-line comparison against equivalent Matplotlib/Seaborn code, see
:doc:`comparison`. For worked, runnable examples of each individual chart —
with rendered output — see :doc:`plot_types`; for multi-panel dashboards
combining several chart types in one call, see :doc:`plots/dashboard`.

Understanding the Argument System
---------------------------------
Expand Down
20 changes: 11 additions & 9 deletions paper.bib
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,17 @@ @book{Wickham2016
doi = {10.1007/978-3-319-24277-4},
}

@inproceedings{VanderPlas2019,
author = {VanderPlas, Jacob and Granger, Brian E. and Heer, Jeffrey and
Moritz, Dominik and Wongsuphasawat, Kanit and Satyanarayan, Arvind and
Lees, Eitan and Timofeev, Ilia and Welsh, Ben and Sievert, Scott},
title = {Altair: Interactive Statistical Visualizations for {Python}},
booktitle = {Proceedings of the 18th Python in Science Conference},
pages = {98--105},
year = {2019},
doi = {10.25080/Majora-7ddc1dd1-00f},
@article{VanderPlas2018,
author = {VanderPlas, Jacob and Granger, Brian E. and Heer, Jeffrey and
Moritz, Dominik and Wongsuphasawat, Kanit and Satyanarayan, Arvind and
Lees, Eitan and Timofeev, Ilia and Welsh, Ben and Sievert, Scott},
title = {Altair: Interactive Statistical Visualizations for {Python}},
journal = {Journal of Open Source Software},
volume = {3},
number = {32},
pages = {1057},
year = {2018},
doi = {10.21105/joss.01057},
}

@software{Plotly2015,
Expand Down
Loading
Loading