{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Top 6 Python Libraries for Visualization: Which One to Use?\n", "\n", "## Motivation {#motivation}\n", "\n", "If you're new to Python visualization, the vast number of libraries and examples available might seem overwhelming. Some popular libraries for visualization include Matplotlib, seaborn, Plotly, Bokeh, Altair, and Pygal.\n", "\n", "When visualizing a DataFrame, choosing the right library can be challenging as different libraries excel in specific cases.\n", "\n", "This article will show the pros and cons of each library. By the end, you will gain a better understanding of their distinct features, making it easier for you to select the optimal library.\n", "\n", "## Quick Reference {#quick-reference}\n", "\n", "Before diving into detailed examples, here's a comprehensive comparison to help you choose the right visualization library for your project:\n", "\n", "| **Feature** | **Matplotlib** | **seaborn** | **Pygal** | **Plotly** | **Altair** | **Bokeh** |\n", "|-------------|----------------|-------------|-----------|------------|------------|-----------|\n", "| **Code Complexity** | High | Low | Low | Medium | Medium | Medium-High |\n", "| **Interactivity** | None (static) | None (static) | Basic hover | Advanced | Grammar-based | Advanced |\n", "| **Chart Types** | Extensive (50+) | Common plots | Basic (14 types) | Extensive (50+) | Statistical focus | Extensive |\n", "| **Web Integration** | Poor | Poor | Good (SVG) | Excellent | Good | Excellent |\n", "| **Customization** | High | Limited | Medium | High | Medium | High |\n", "| **Best Use Cases** | Publications, Papers | Statistical plots | Lightweight web apps | Dashboards, BI | Data exploration | Interactive web apps |\n", "| **Dependencies** | Moderate | Moderate | Minimal | Heavy | Moderate | Moderate |\n", "\n", "### Quick Decision Guide\n", "\n", "Here is a quick decision guide to help you choose the right visualization library for your project:\n", "\n", "**Choose Matplotlib when:** Creating publication-quality static plots, need unlimited customization control, working on academic papers or research\n", "\n", "**Choose seaborn when:** Making statistical visualizations quickly, want beautiful plots with minimal code, working with pandas DataFrames \n", "\n", "**Choose Pygal when:** Building lightweight web applications, need SVG vector graphics that scale perfectly, want minimal dependencies and fast loading\n", "\n", "**Choose Plotly when:** You need interactive visualizations with hover tooltips, zooming, and clickable legends, or when building web dashboards and applications that require user engagement with the data\n", "\n", "**Choose Altair when:** Doing statistical data exploration, want grammar-of-graphics approach, need linked/coordinated visualizations, working primarily in Jupyter notebooks\n", "\n", "**Choose Bokeh when:** Building complex interactive web applications, need linked plots and advanced interactions, want fine-grained control over web deployment, creating custom visualization tools\n", "\n", "\n", "## Matplotlib {#matplotlib}\n", "\n", "[Matplotlib](https://matplotlib.org/) is probably the most common Python library for visualizing data. Almost everyone interested in data science has likely utilized Matplotlib at least once.\n", "\n", "### Pros\n", "\n", "#### Easy to interpret data properties\n", "\n", "When analyzing data, it's often helpful to get a quick overview of its distribution.\n", "\n", "For example, if you want to examine the distribution of the top 100 users with the most followers, Matplotlib is typically sufficient." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", "\n", "new_profile = pd.read_csv(\n", " \"https://gist.githubusercontent.com/khuyentran1401/98658198f0ef0cb12abb34b4f2361fd8/raw/ece16eb32e1b41f5f20c894fb72a4c198e86a5ea/github_users.csv\"\n", ")\n", "\n", "top_followers = new_profile.sort_values(by=\"followers\", axis=0, ascending=False)[:100]\n", "\n", "fig = plt.figure()\n", "\n", "plt.bar(top_followers.user_name, top_followers.followers)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Despite Matplotlib's suboptimal x-axis representation, the graph provides a clear understanding of the data distribution.\n", "\n", "#### Versatility\n", "\n", "Matplotlib is very versatile and capable of generating a wide range of graph types. The Matplotlib's [website](https://matplotlib.org/tutorials/introductory/sample_plots.html) offers comprehensive documentation and a gallery of various graphs, making it easy to find tutorials for virtually any type of plot." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig = plt.figure()\n", "\n", "plt.text(\n", " 0.6,\n", " 0.7,\n", " \"learning\",\n", " size=40,\n", " rotation=20.0,\n", " ha=\"center\",\n", " va=\"center\",\n", " bbox={\n", " \"boxstyle\": \"round\",\n", " \"ec\": (1.0, 0.5, 0.5),\n", " \"fc\": (1.0, 0.8, 0.8),\n", " },\n", ")\n", "\n", "plt.text(\n", " 0.55,\n", " 0.6,\n", " \"machine\",\n", " size=40,\n", " rotation=-25.0,\n", " ha=\"right\",\n", " va=\"top\",\n", " bbox={\n", " \"boxstyle\": \"square\",\n", " \"ec\": (1.0, 0.5, 0.5),\n", " \"fc\": (1.0, 0.8, 0.8),\n", " },\n", ")\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Animation capabilities\n", "\n", "Matplotlib provides powerful animation features through the `matplotlib.animation` module, enabling dynamic visualizations that evolve over time. Here are three examples that showcase the animation capabilities:\n", "\n", "**Animated Line Plot with Real-time Data**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.animation as animation\n", "from IPython.display import Image\n", "\n", "fig, ax = plt.subplots()\n", "\n", "x = np.arange(0, 2 * np.pi, 0.01)\n", "(line,) = ax.plot(x, np.sin(x))\n", "ax.set_ylim(-1.5, 1.5)\n", "ax.set_title(\"Animated Sine Wave\")\n", "\n", "\n", "def animate(frame):\n", " line.set_ydata(np.sin(x + frame / 10.0))\n", " return (line,)\n", "\n", "\n", "ani = animation.FuncAnimation(fig, animate, frames=10, interval=50, blit=True)\n", "ani.save(\"sine_wave_animation.gif\", writer=\"pillow\", fps=10)\n", "\n", "Image(\"sine_wave_animation.gif\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Animated Bar Chart Race**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create sample data for animation\n", "categories = [\"Product A\", \"Product B\", \"Product C\", \"Product D\"]\n", "fig, ax = plt.subplots()\n", "\n", "\n", "def animate_bars(frame):\n", " ax.clear()\n", " # Simulate changing data over time\n", " values = [np.sin(frame / 10 + i) * 50 + 60 for i in range(4)]\n", " colors = plt.cm.viridis(np.linspace(0, 1, 4))\n", "\n", " bars = ax.bar(categories, values, color=colors)\n", " ax.set_ylim(0, 120)\n", " ax.set_title(f\"Sales Performance - Month {frame + 1}\")\n", "\n", " # Add value labels on bars\n", " for bar, value in zip(bars, values, strict=False):\n", " height = bar.get_height()\n", " ax.text(\n", " bar.get_x() + bar.get_width() / 2.0,\n", " height + 2,\n", " f\"{value:.0f}\",\n", " ha=\"center\",\n", " va=\"bottom\",\n", " )\n", "\n", " return bars\n", "\n", "\n", "ani = animation.FuncAnimation(fig, animate_bars, frames=50, interval=100)\n", "ani.save(\"bar_race_animation.gif\", writer=\"pillow\", fps=5)\n", "\n", "Image(\"bar_race_animation.gif\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These animations demonstrate Matplotlib's versatility for creating engaging dynamic visualizations, from scientific data trends to business dashboards.\n", "\n", "#### Publication-quality output\n", "\n", "Matplotlib excels at creating high-resolution, publication-ready visualizations suitable for academic papers, research reports, and professional presentations. The library provides precise control over figure size, DPI, and output formats (PNG, PDF, SVG, EPS), ensuring your plots meet the strict requirements of scientific journals and publications." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Set publication-quality parameters\n", "plt.rcParams.update({\n", " \"font.size\": 10, # Standard academic paper font size\n", " \"font.family\": \"serif\", # Traditional serif fonts for publications\n", " \"axes.linewidth\": 1.2, # Thicker axes for better print visibility\n", " \"figure.dpi\": 300, # High resolution for sharp display\n", " \"savefig.dpi\": 300, # High resolution for saved files\n", " \"savefig.bbox\": \"tight\" # Remove extra whitespace when saving\n", "})\n", "\n", "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n", "fig.suptitle(\"GitHub User Analysis: Publication Ready\", fontsize=14, fontweight=\"bold\")\n", "\n", "# Subplot 1: Distribution histogram\n", "top_users = new_profile.sort_values(\"followers\", ascending=False)[:50]\n", "ax1.hist(top_users[\"followers\"], bins=15, alpha=0.7, color=\"steelblue\", edgecolor=\"black\")\n", "ax1.set_xlabel(\"Followers Count\")\n", "ax1.set_ylabel(\"Frequency\")\n", "ax1.set_title(\"A) Follower Distribution\")\n", "ax1.grid(True, alpha=0.3)\n", "\n", "# Subplot 2: Correlation scatter\n", "ax2.scatter(top_users[\"followers\"], top_users[\"total_stars\"], alpha=0.6, s=30)\n", "ax2.set_xlabel(\"Followers\")\n", "ax2.set_ylabel(\"Total Stars\")\n", "ax2.set_title(\"B) Followers vs Stars Correlation\")\n", "ax2.grid(True, alpha=0.3)\n", "\n", "# Save as publication-ready formats\n", "plt.tight_layout()\n", "plt.savefig(\"github_analysis.pdf\", format=\"pdf\", bbox_inches=\"tight\")\n", "plt.savefig(\"github_analysis.eps\", format=\"eps\", bbox_inches=\"tight\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Cons\n", "\n", "#### Steep learning curve\n", "\n", "Matplotlib's extensive functionality comes with complexity. New users often find the syntax overwhelming, especially when transitioning from point-and-click visualization tools. Understanding the figure-axes hierarchy and object-oriented vs. pyplot interfaces requires significant time investment.\n", "\n", "#### Extensive styling needed for publication-ready common plots\n", "\n", "While Matplotlib supports virtually any chart type, producing visually polished versions of standard plots like histograms, scatter plots, or bar charts requires substantial customization work.\n", "\n", "To make common visualizations suitable for presentations or sharing, you must manually style numerous elements: axis formatting, color schemes, legends, annotations, and layout spacing. Matplotlib's low-level interface provides complete control but assumes users will configure all visual aspects from scratch.\n", "\n", "For example, by default, the heatmap doesn't have the x-axis and y-axis labels and annotations." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "num_features = new_profile.select_dtypes(\"int64\")\n", "correlation = num_features.corr()\n", "\n", "fig, ax = plt.subplots()\n", "im = plt.imshow(correlation, cmap=\"coolwarm\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Creating a readable heatmap in Matplotlib requires several manual steps:\n", "\n", "- Define the visualization layout and color scheme\n", "- Manually position axis ticks and labels\n", "- Loop through each cell to add number annotations with appropriate formatting" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "num_features = new_profile.select_dtypes(\"int64\")\n", "correlation = num_features.corr()\n", "\n", "fig, ax = plt.subplots()\n", "im = plt.imshow(correlation, cmap=\"coolwarm\")\n", "\n", "\n", "ax.set_xticks(np.arange(len(correlation.columns)))\n", "ax.set_yticks(np.arange(len(correlation.columns)))\n", "ax.set_xticklabels(correlation.columns)\n", "ax.set_yticklabels(correlation.columns)\n", "\n", "# Add number annotations manually\n", "for i in range(len(correlation.columns)):\n", " for j in range(len(correlation.columns)):\n", " # Choose text color based on background intensity\n", " ax.text(\n", " j,\n", " i,\n", " f\"{correlation.iloc[i, j]:.2f}\",\n", " ha=\"center\",\n", " va=\"center\",\n", " color=\"black\",\n", " fontweight=\"bold\",\n", " )\n", "\n", "plt.setp(ax.get_xticklabels(), rotation=45, ha=\"right\", rotation_mode=\"anchor\")\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Creating a basic annotated heatmap shouldn't require this many lines of manual setup and configuration.\n", "\n", "#### Manual statistical processing required\n", "\n", "Unlike higher-level libraries like seaborn, Matplotlib doesn't automatically handle statistical computations or data preprocessing. You must manually perform all data grouping, filtering, aggregations, and statistical calculations before plotting." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import seaborn as sns\n", "from scipy import stats\n", "\n", "# Load penguins dataset for this example\n", "penguins = sns.load_dataset(\"penguins\")\n", "\n", "# Matplotlib requires manual statistical processing for regression analysis\n", "# Filter out missing values manually\n", "penguins_clean = penguins.dropna(subset=[\"bill_length_mm\", \"flipper_length_mm\"])\n", "\n", "# Manual regression calculation (no automatic trend estimation)\n", "x = penguins_clean[\"bill_length_mm\"].values\n", "y = penguins_clean[\"flipper_length_mm\"].values\n", "\n", "# Compute regression statistics manually\n", "slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)\n", "\n", "# Manual confidence interval calculation with proper statistical formula\n", "n = len(x)\n", "x_mean = np.mean(x)\n", "sxx = np.sum((x - x_mean) ** 2)\n", "y_pred = slope * x + intercept\n", "residuals = y - y_pred\n", "mse = np.sum(residuals**2) / (n - 2)\n", "std_error_regression = np.sqrt(mse)\n", "\n", "# Create confidence bands with proper varying width\n", "x_smooth = np.linspace(x.min(), x.max(), 100)\n", "y_smooth = slope * x_smooth + intercept\n", "\n", "# Calculate standard error for each point on the smooth line\n", "se_y = std_error_regression * np.sqrt(1 / n + (x_smooth - x_mean) ** 2 / sxx)\n", "\n", "# Use t-distribution for 95% confidence interval\n", "t_val = stats.t.ppf(0.975, df=n - 2) # 95% confidence\n", "ci = t_val * se_y\n", "\n", "# Plot with manually computed statistics\n", "plt.figure(figsize=(8, 6))\n", "plt.scatter(x, y, alpha=0.6, s=20, color=\"#1f77b4\", label=\"Data points\")\n", "plt.plot(x_smooth, y_smooth, color=\"#1f77b4\", linewidth=2, label=\"Regression line\")\n", "plt.fill_between(\n", " x_smooth,\n", " y_smooth - ci,\n", " y_smooth + ci,\n", " alpha=0.2,\n", " color=\"#1f77b4\",\n", " label=\"95% Confidence interval\",\n", ")\n", "plt.xlabel(\"bill_length_mm\")\n", "plt.ylabel(\"flipper_length_mm\")\n", "plt.title(\"Penguins Regression (Manual statistical processing required)\")\n", "plt.legend()\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Key Takeaways\n", "\n", "Matplotlib is capable of producing any plot, but creating complex plots often requires more code compared to other libraries.\n", "\n", "## seaborn {#seaborn}\n", "\n", "[seaborn](https://seaborn.pydata.org/) is a Python data visualization library built on top of Matplotlib. It offers a higher-level interface, simplifying the process of creating visually appealing plots.\n", "\n", "### Pros\n", "\n", "#### Reduced code\n", "\n", "seaborn offers a higher-level interface that simplifies plot creation compared to Matplotlib. Since it's designed specifically for pandas DataFrames, you can create attractive visualizations with minimal code.\n", "\n", "For instance, using the same data as before, we can create a nice heatmap without explicitly setting the x and y labels:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import seaborn as sns\n", "\n", "# Load penguins dataset for seaborn examples\n", "penguins = sns.load_dataset(\"penguins\")\n", "\n", "# Use numeric features only for correlation\n", "correlation = num_features.corr()\n", "\n", "sns.heatmap(correlation, annot=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This results in a more visually appealing heatmap without the need for additional configuration.\n", "\n", "#### Statistical plots with automatic processing\n", "\n", "seaborn excels at automatically performing statistical computations and aggregations, eliminating the need for manual data processing. It handles statistical estimation, uncertainty visualization, and data transformations behind the scenes.\n", "\n", "**Automatic statistical estimation with confidence intervals:**\n", "\n", "Unlike the extensive manual statistical processing required in Matplotlib (as shown above), seaborn can create a scatter plot with trend line and confidence interval using just one function call:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Load penguins dataset and automatically estimate trend lines with confidence intervals\n", "sns.lmplot(\n", " data=penguins,\n", " x=\"bill_length_mm\",\n", " y=\"flipper_length_mm\",\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Other examples of seaborn's statistical plots:\n", "\n", "**Automatic distribution fitting and visualization:**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Automatically computes and overlays histogram, KDE, and rug plots\n", "sns.displot(data=penguins, x=\"flipper_length_mm\", kde=True, rug=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Automatic marginal distribution and correlation computation:**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Automatically computes scatter plot, marginal histograms, and correlation coefficient\n", "sns.jointplot(data=penguins, x=\"flipper_length_mm\", y=\"body_mass_g\", kind=\"reg\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Automatic pairwise relationships:**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Automatically creates scatter plots for all numeric pairs with marginal histograms\n", "sns.pairplot(data=penguins, hue=\"species\", palette=\"deep\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Distribution visualization with kernel density:**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Automatically fits KDE and shows distribution shape\n", "sns.violinplot(data=penguins, x=\"species\", y=\"body_mass_g\", palette=\"coolwarm\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Statistical summary with quartiles:**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Automatically computes median, quartiles, and outliers\n", "sns.boxplot(data=penguins, x=\"species\", y=\"body_mass_g\", palette=\"coolwarm\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These examples demonstrate how seaborn handles complex statistical processing automatically, saving significant manual computation and code complexity.\n", "\n", "### Cons\n", "\n", "#### Limited customization compared to Matplotlib\n", "\n", "seaborn excels at creating attractive plots quickly but sacrifices fine-grained control for simplicity. Advanced customizations like precise positioning, custom annotations, or non-standard modifications require dropping to matplotlib's lower level, defeating seaborn's simplicity purpose.\n", "\n", "In the following example, while seaborn creates clean line plots effortlessly, adding highlighted periods and custom annotations requires matplotlib's lower-level interface." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Load flights dataset - contains smooth passenger trends\n", "flights = sns.load_dataset(\"flights\")\n", "\n", "# seaborn creates smooth line plot \n", "sns.lineplot(data=flights, x=\"year\", y=\"passengers\")\n", "\n", "# But adding business annotations requires matplotlib\n", "ax = plt.gca()\n", "\n", "# Highlight jet age introduction period\n", "ax.axvspan(1955, 1958, alpha=0.2, color=\"green\", label=\"Jet Age Introduction\")\n", "\n", "# Add annotation for technical achievement\n", "ax.annotate(\"Jet Engine Impact\", \n", " xy=(1957, 400), \n", " xytext=(1952, 450),\n", " arrowprops={\"arrowstyle\": \"->\", \"color\": \"red\", \"lw\": 2},\n", " fontsize=12, color=\"red\", weight=\"bold\")\n", "\n", "# Custom legend with mixed elements\n", "ax.legend([\"Passenger Trend\", \"Jet Age\"], loc=\"upper left\")\n", "plt.title(\"Air Travel Growth with Historical Context\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Limited plot type collection\n", "\n", "While seaborn excels at statistical plots, it lacks the breadth of Matplotlib's visualization options for specialized scientific or custom chart types.\n", "\n", "#### No interactive or animated features\n", "\n", "seaborn is designed exclusively for static statistical visualizations and lacks any built-in support for interactivity or animations.\n", "\n", "### Key Takeaways\n", "\n", "seaborn is a higher-level version of Matplotlib. Even though it does not have a wide collection as Matplotlib, seaborn makes popular plots such as bar plot, box plot, heatmap, etc look pretty in less code.\n", "\n", "## Pygal {#pygal}\n", "\n", "[Pygal](https://github.com/Kozea/pygal) is a lightweight Python library that generates scalable vector graphics (SVG) charts. Built specifically for web applications, Pygal creates interactive visualizations with minimal dependencies and extremely fast rendering.\n", "\n", "### Pros\n", "\n", "#### SVG vector graphics with perfect scaling\n", "\n", "Pygal generates pure SVG output that scales perfectly across all devices and screen sizes. Unlike bitmap images from other libraries, SVG charts maintain crisp quality at any zoom level.\n", "\n", "This makes Pygal ideal for responsive web applications where charts need to look sharp on both mobile devices and large desktop monitors:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pygal\n", "\n", "# Create a bar chart showing top GitHub users by followers\n", "top_followers = new_profile.sort_values(by=\"followers\", ascending=False)[:10]\n", "\n", "bar_chart = pygal.Bar(\n", " title=\"Top 10 GitHub Users by Followers\",\n", " x_title=\"Users\",\n", " y_title=\"Followers\"\n", ")\n", "bar_chart.x_labels = top_followers[\"user_name\"].tolist()\n", "bar_chart.add(\"Followers\", top_followers[\"followers\"].tolist())\n", "\n", "# Save chart as SVG file\n", "bar_chart.render_to_file(\"github_top_users.svg\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The resulting SVG can be embedded directly into HTML without additional dependencies or image files.\n", "\n", "#### Built-in interactivity with hover tooltips\n", "\n", "Every Pygal chart includes hover tooltips by default through native SVG interactivity. Users can explore data points without requiring additional JavaScript libraries, frameworks, or configuration.\n", "\n", "The bar chart above automatically displays precise follower counts when you hover over each bar, providing instant data exploration without any setup required.\n", "\n", "To see the chart in a browser, we can use the following code:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bar_chart.render_in_browser()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To see Pygal's interactivity in Jupyter notebooks, include the following JavaScript dependencies:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# For interactive display in notebooks, we need to wrap the SVG with JavaScript\n", "from IPython.display import HTML, display\n", "\n", "\n", "def display_pygal_chart(chart):\n", " \"\"\"Display a Pygal chart with full interactivity in Jupyter notebooks\"\"\"\n", " html_template = \"\"\"\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
{chart}
\n", " \n", " \n", " \"\"\"\n", " rendered = chart.render(is_unicode=True)\n", " display(HTML(html_template.format(chart=rendered)))\n", "\n", "# Display the chart with full interactivity (hover tooltips, etc.)\n", "display_pygal_chart(bar_chart)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Professional styling for common chart types\n", "\n", "One of Pygal's key advantages is how it automatically enhances common chart types with professional aesthetics and built-in interactivity. Here are some examples:\n", "\n", "**Radar Chart - Multi-Dimensional User Comparison**\n", "\n", "Compare multiple users across different metrics simultaneously:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create radar chart for multi-dimensional comparison\n", "top_5_users = new_profile.sort_values(by=\"followers\", ascending=False)[:5]\n", "\n", "radar_chart = pygal.Radar(title=\"Top 5 Users: Multi-Metric Comparison\", fill=True)\n", "\n", "# Normalize metrics to 0-100 scale for better comparison\n", "max_followers = top_5_users[\"followers\"].max()\n", "max_stars = top_5_users[\"total_stars\"].max()\n", "max_forks = top_5_users[\"forks\"].max()\n", "max_contrib = top_5_users[\"contribution\"].max()\n", "\n", "for _, user in top_5_users.iterrows():\n", " radar_chart.add(\n", " user[\"user_name\"],\n", " [\n", " (user[\"followers\"] / max_followers) * 100,\n", " (user[\"total_stars\"] / max_stars) * 100,\n", " (user[\"forks\"] / max_forks) * 100,\n", " (user[\"contribution\"] / max_contrib) * 100,\n", " ],\n", " )\n", "\n", "radar_chart.x_labels = [\"Followers\", \"Stars\", \"Forks\", \"Contributions\"]\n", "display_pygal_chart(radar_chart)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Box Plot - Age Distribution by Passenger Class**\n", "\n", "Compare age distributions across different Titanic passenger classes:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Load titanic dataset\n", "titanic = sns.load_dataset(\"titanic\")\n", "\n", "# Filter out passengers with missing age data\n", "titanic_with_age = titanic.dropna(subset=[\"age\"])\n", "\n", "# Create box plot comparing age across passenger classes\n", "box_plot = pygal.Box(\n", " title=\"Titanic: Age Distribution by Passenger Class\",\n", " y_title=\"Age (years)\",\n", " box_mode=\"tukey\", # Shows outliers beyond 1.5 IQR\n", ")\n", "\n", "# Get passenger classes and add data for each\n", "classes = [\"First\", \"Second\", \"Third\"]\n", "for class_name in classes:\n", " class_passengers = titanic_with_age[titanic_with_age[\"class\"] == class_name]\n", " age_data = class_passengers[\"age\"].tolist() # Pygal expects lists\n", "\n", " # Add class with passenger count for context\n", " passenger_count = len(class_passengers)\n", " box_plot.add(f\"{class_name} Class\", age_data)\n", "\n", "display_pygal_chart(box_plot)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Minimal dependencies and fast loading\n", "\n", "Pygal requires minimal dependencies compared to other visualization libraries, making it ideal for lightweight applications where deployment size and startup speed matter.\n", "\n", "### Cons\n", "\n", "#### Limited to 14 basic chart types\n", "\n", "Pygal's biggest limitation is its restricted chart type collection. With only 14 basic options (bar, line, pie, scatter, etc.), it lacks advanced statistical visualizations like violin plots, heatmaps, or complex multi-axis charts that are essential for sophisticated data analysis and scientific visualization.\n", "\n", "### Key Takeaways\n", "\n", "Pygal excels when you need lightweight, scalable charts for web applications. Its SVG output, built-in interactivity, and minimal dependencies make it perfect for responsive dashboards, but consider other libraries for complex statistical analysis or specialized chart types.\n", "\n", "## Plotly {#plotly}\n", "\n", "[Plotly's Python](https://plotly.com/python/) graphing library provides an effortless way to create interactive and high-quality graphs. It offers a range of chart types similar to Matplotlib and seaborn, including line plots, scatter plots, area charts, bar charts, and more.\n", "\n", "### Pros\n", "\n", "#### Easily create beautiful interactive plots\n", "\n", "Plotly excels at creating interactive visualizations with minimal code. Plotly Express makes this especially easy, allowing you to create beautiful interactive plots with just a single line of Python code:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import plotly.express as px\n", "\n", "fig = px.scatter(\n", " new_profile[:100],\n", " x=\"followers\",\n", " y=\"total_stars\",\n", " color=\"forks\",\n", " size=\"contribution\",\n", ")\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Simplicity in complex plots\n", "\n", "Plotly simplifies the creation of complex plots that might be challenging with other libraries.\n", "\n", "For example, if we want to visualize the locations of GitHub users on a map provided with their latitudes and longitudes, we can plot the locations on a map in a single line of code:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "location_df = pd.read_csv(\n", " \"https://gist.githubusercontent.com/khuyentran1401/ce61bbad3bc636bf2548d70d197a0e3f/raw/ab1b1a832c6f3e01590a16231ba25ca5a3d761f3/location_df.csv\",\n", " index_col=0,\n", ")\n", "\n", "m = px.scatter_geo(\n", " location_df,\n", " lat=\"latitude\",\n", " lon=\"longitude\",\n", " color=\"total_stars\",\n", " size=\"forks\",\n", " hover_data=[\"user_name\", \"followers\"],\n", " title=\"Locations of Top Users\",\n", ")\n", "\n", "m.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, the color of the bubbles represents the number of stars, while the size corresponds to the number of forks.\n", "\n", "#### Business intelligence features\n", "\n", "Plotly offers enterprise-grade visualization features including interactive drill-down charts, real-time data updates, and cross-filtering capabilities. Business users can create comprehensive dashboards that allow stakeholders to explore data relationships, filter across multiple dimensions, and export insights for presentations.\n", "\n", "The following example shows an interactive sunburst chart for hierarchical data exploration. Users can click through data layers from regions to departments and view detailed revenue breakdowns on hover." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create hierarchical data for drill-down\n", "df = pd.DataFrame({\n", " \"Region\": [\"North\", \"North\", \"South\", \"South\"],\n", " \"Department\": [\"Sales\", \"Marketing\", \"Sales\", \"Marketing\"],\n", " \"Revenue\": [250000, 180000, 200000, 150000],\n", " \"Quarter\": [\"Q4\", \"Q4\", \"Q4\", \"Q4\"]\n", "})\n", "\n", "# Sunburst chart for hierarchical drill-down\n", "fig = px.sunburst(df, path=[\"Region\", \"Department\"], \n", " values=\"Revenue\",\n", " title=\"Revenue Drill-down: Region → Department\")\n", "\n", "# Add crossfilter-style hover interactions\n", "fig.update_traces(textinfo=\"label+percent parent\")\n", "fig.update_layout(height=500)\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following example shows an animated bubble chart with timeline controls. Users can navigate through decades of data evolution with custom timeline controls, play/pause buttons, and range sliders." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df = px.data.gapminder()\n", "\n", "fig = px.scatter(\n", " df,\n", " x=\"gdpPercap\",\n", " y=\"lifeExp\",\n", " animation_frame=\"year\",\n", " size=\"pop\",\n", " color=\"continent\",\n", " log_x=True,\n", " size_max=55,\n", " range_x=[100, 100000],\n", " range_y=[25, 90],\n", " title=\"GDP vs Life Expectancy by Year\",\n", ")\n", "\n", "\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Cons\n", "\n", "#### Heavy dependencies\n", "\n", "Plotly comes with substantial dependencies that can significantly increase your project's size and deployment complexity. The full Plotly package includes multiple rendering engines, which may be overkill for simple visualization needs and can slow down application startup times.\n", "\n", "### Key Takeaways\n", "\n", "Plotly excels at creating interactive and publication-quality visualizations with minimal code required. While it offers a wide range of visualizations and simplifies complex plots, consider the substantial dependencies that can increase project size and deployment complexity.\n", "\n", "## Altair {#altair}\n", "\n", "[Altair](https://altair-viz.github.io/) is a powerful declarative statistical visualization library for Python that is based on Vega-Lite. It shines when it comes to creating plots that require extensive statistical transformations.\n", "\n", "### Pros\n", "\n", "#### Simple visualization grammar\n", "\n", "Altair utilizes intuitive grammar for creating visualizations. You only need to specify the links between data columns and encoding channels, and the rest of the plotting is handled automatically. This simplicity makes visualizing information fast and intuitive.\n", "\n", "For instance, to count the number of people in each class using the Titanic dataset:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import altair as alt\n", "\n", "titanic = sns.load_dataset(\"titanic\")\n", "\n", "alt.Chart(titanic).mark_bar().encode(alt.X(\"class\"), y=\"count()\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Altair's concise syntax allows you to focus on the data and its relationships, resulting in efficient and expressive visualizations.\n", "\n", "#### Easy data transformation\n", "\n", "Altair makes it effortless to perform data transformations while creating charts.\n", "\n", "For example, if you want to find the average age of each sex in the Titanic dataset, you can perform the transformation within the code itself:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "hireable = (\n", " alt.Chart(titanic)\n", " .mark_bar()\n", " .encode(x=\"sex:N\", y=\"mean_age:Q\")\n", " .transform_aggregate(mean_age=\"mean(age)\", groupby=[\"sex\"])\n", ")\n", "\n", "hireable" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Altair's `transform_aggregate()` function enables you to aggregate data on the fly and use the results in your visualization.\n", "\n", "You can also specify the data type, such as nominal (categorical data without any order) or quantitative (measures of values), using the `:N` or `:Q` notation.\n", "\n", "See a full list of data transformations [here](https://altair-viz.github.io/user_guide/transform/index.html).\n", "\n", "#### Linked plots\n", "\n", "Altair provides impressive capabilities for linking multiple plots together. You can use selections to filter the contents of the attached plots based on user interactions.\n", "\n", "For example, to visualize the number of people in each class within a selected interval on a scatter plot:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "brush = alt.selection_interval()\n", "\n", "points = (\n", " alt.Chart(titanic)\n", " .mark_point()\n", " .encode(\n", " x=\"age:Q\",\n", " y=\"fare:Q\",\n", " color=alt.condition(brush, \"class:N\", alt.value(\"lightgray\")),\n", " )\n", " .add_params(brush)\n", ")\n", "\n", "bars = (\n", " alt.Chart(titanic)\n", " .mark_bar()\n", " .encode(y=\"class:N\", color=\"class:N\", x=\"count(class):Q\")\n", " .transform_filter(brush)\n", ")\n", "\n", "points & bars" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you select an interval within the scatter plot, the bar chart dynamically updates to reflect the filtered data. Altair's ability to link plots allows for highly interactive visualizations with on-the-fly calculations, without the need for a running Python server.\n", "\n", "### Cons\n", "\n", "#### Limited styling options\n", "\n", "Altair's simple charts, such as bar charts, may not look as styled as those in libraries like seaborn or Plotly unless you specify custom styling.\n", "\n", "#### Dataset size limitations\n", "\n", "Altair recommends aggregating your data prior to visualization when dealing with datasets exceeding 5000 samples. Handling larger datasets may require additional steps to manage data size and complexity.\n", "\n", "### Key Takeaways\n", "\n", "Altair excels at statistical visualization with intuitive grammar and linked plots that enable interactive exploration. While it simplifies complex data transformations, its styling limitations and constraints with large datasets may require workarounds for specialized visualization needs.\n", "\n", "## Bokeh {#bokeh}\n", "\n", "[Bokeh](https://bokeh.org/) is a highly flexible interactive visualization library designed for web browsers.\n", "\n", "### Pros\n", "\n", "#### Interactive version of Matplotlib\n", "\n", "Bokeh stands out as the most similar library to Matplotlib when it comes to interactive visualization. While Matplotlib is a low-level visualization library, Bokeh offers both high-level and low-level interfaces. With Bokeh, you can create sophisticated plots similar to Matplotlib but with fewer lines of code and higher resolution.\n", "\n", "For example, the circle plot of Matplotlib:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "\n", "x_data = [1, 2, 3, 4, 5]\n", "y_data = [2, 5, 8, 2, 7]\n", "\n", "for x_point, y_point in zip(x_data, y_data, strict=False):\n", " ax.add_patch(\n", " plt.Circle((x_point, y_point), 0.5, edgecolor=\"#f03b20\", facecolor=\"#9ebcda\", alpha=0.8)\n", " )\n", "\n", "# Use adjustable='box-forced' to make the plot area square-shaped as well.\n", "ax.set_aspect(\"equal\", adjustable=\"datalim\")\n", "ax.set_xbound(3, 4)\n", "\n", "ax.plot() # Causes an autoscale update.\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "…can be achieved with better resolution and interactivity using Bokeh:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.io import output_notebook, show\n", "from bokeh.models.glyphs import Scatter\n", "from bokeh.plotting import figure\n", "\n", "output_notebook()\n", "\n", "plot = figure(tools=\"tap\", title=\"Select a circle\")\n", "renderer = plot.scatter([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], size=50, marker=\"circle\")\n", "\n", "selected_circle = Scatter(size=50, fill_alpha=1, fill_color=\"firebrick\", line_color=None, marker=\"circle\")\n", "nonselected_circle = Scatter(size=50, fill_alpha=0.2, fill_color=\"blue\", line_color=\"firebrick\", marker=\"circle\")\n", "\n", "renderer.selection_glyph = selected_circle\n", "renderer.nonselection_glyph = nonselected_circle\n", "\n", "show(plot)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Link between plots\n", "\n", "Bokeh makes it incredibly easy to establish links between plots. Changes applied to one plot can be automatically reflected in another plot with similar variables. This feature allows for exploring relationships between multiple plots.\n", "\n", "For instance, if you create three graphs side by side and want to observe their relationship, you can utilize linked brushing:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.layouts import gridplot\n", "from bokeh.models import ColumnDataSource\n", "\n", "source = ColumnDataSource(new_profile)\n", "\n", "TOOLS = \"box_select,lasso_select,help\"\n", "TOOLTIPS = [\n", " (\"user\", \"@user_name\"),\n", " (\"followers\", \"@followers\"),\n", " (\"following\", \"@following\"),\n", " (\"forks\", \"@forks\"),\n", " (\"contribution\", \"@contribution\"),\n", "]\n", "\n", "s1 = figure(tooltips=TOOLTIPS, title=None, tools=TOOLS)\n", "s1.scatter(x=\"followers\", y=\"following\", source=source)\n", "\n", "s2 = figure(tooltips=TOOLTIPS, title=None, tools=TOOLS)\n", "s2.scatter(x=\"followers\", y=\"forks\", source=source)\n", "\n", "s3 = figure(tooltips=TOOLTIPS, title=None, tools=TOOLS)\n", "s3.scatter(x=\"followers\", y=\"contribution\", source=source)\n", "\n", "p = gridplot([[s1, s2, s3]])\n", "show(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "By utilizing `ColumnDataSource`, the data can be shared among plots. Thus, when a change is applied to one plot, the other plots automatically update accordingly.\n", "\n", "#### Fine-grained control over web deployment\n", "\n", "Bokeh offers exceptional control over visualization deployment in web applications. You can embed plots in existing websites, create standalone HTML files, or build complete web applications with custom servers, providing seamless integration flexibility.\n", "\n", "Here's how to embed a Bokeh plot in your existing website. First, create your Bokeh visualization as usual:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.plotting import figure\n", "\n", "# Create sample sales data\n", "df = pd.DataFrame({\n", " \"sales\": [100, 150, 200, 120, 180, 250, 300, 220],\n", " \"profit\": [20, 30, 45, 25, 40, 60, 75, 50],\n", " \"category\": [\"A\", \"B\", \"A\", \"B\", \"A\", \"B\", \"A\", \"B\"]\n", "})\n", "source = ColumnDataSource(df)\n", "\n", "# Create plot\n", "p = figure(title=\"Sales Analysis Widget\", width=500, height=300)\n", "p.scatter(\"sales\", \"profit\", source=source, size=10, alpha=0.6)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, generate the HTML components for web embedding:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.embed import components\n", "\n", "# Generate components for embedding\n", "script, div = components(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, integrate these components into your website. Add the Bokeh CDN to your HTML template's head section:\n", "\n", "```html\n", "\n", " \n", "\n", "```\n", "\n", "Then include the plot components in your page body where you want the visualization to appear:\n", "\n", "```html\n", "\n", " \n", " {{ plot_div|safe }} \n", " {{ plot_script|safe }} \n", "\n", "```\n", "\n", "### Cons\n", "\n", "#### Verbose code requirements\n", "\n", "While Bokeh offers powerful customization, it demands considerably more setup code to make even simple plots look professional compared to simpler alternatives like seaborn and Plotly.\n", "\n", "For the same titanic count plot, you must transform the data beforehand and manually configure bar width and colors to achieve an attractive result.\n", "\n", "If we didn't add width for the bar graph, the graph would look like this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.palettes import Spectral6\n", "from bokeh.transform import factor_cmap\n", "\n", "titanic_groupby = titanic.groupby(\"class\")[\"survived\"].sum().reset_index()\n", "\n", "p = figure(x_range=list(titanic_groupby[\"class\"]))\n", "p.vbar(\n", " x=\"class\",\n", " top=\"survived\",\n", " source=titanic_groupby,\n", " fill_color=factor_cmap(\n", " \"class\", palette=Spectral6, factors=list(titanic_groupby[\"class\"])\n", " ),\n", ")\n", "show(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Thus, we need to manually adjust the dimensions to make the plot nicer:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p = figure(x_range=list(titanic_groupby[\"class\"]))\n", "p.vbar(\n", " x=\"class\",\n", " top=\"survived\",\n", " width=0.9,\n", " source=titanic_groupby,\n", " fill_color=factor_cmap(\n", " \"class\", palette=Spectral6, factors=list(titanic_groupby[\"class\"])\n", " ),\n", ")\n", "show(p)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3", "path": "/Users/khuyentran/codecut_content/codecut_articles/.venv/share/jupyter/kernels/python3" } }, "nbformat": 4, "nbformat_minor": 4 }