Combining Figures¶
This section showcases how to combine multiple charts into a grid layout using the datachart.utils.FigureGridLayout function.
The examples sequentially build on each other, going from simple to more complex.
import numpy as np
Introduction¶
The FigureGridLayout function allows you to combine multiple chart figures into a single grid layout. This is useful when you want to:
- Compare multiple datasets side by side
- Create dashboards with different chart types
- Display related visualizations together
- Share axes across multiple charts for easier comparison
Let's start by importing the necessary functions:
from datachart.utils import FigureGridLayout
from datachart.charts import LineChart, BarChart, ScatterChart
from datachart.constants import FIG_SIZE
Function Parameters¶
The FigureGridLayout function accepts the following parameters:
| Parameter | Type | Description |
|---|---|---|
charts |
List[Dict[str, Any]] |
List of chart configuration dictionaries. Each dict must contain a "figure" key with the matplotlib Figure object, and optionally a "layout_spec" key for custom grid positioning. |
title |
Optional[str] |
Optional title for the combined figure. |
max_cols |
int |
Maximum number of columns in the grid layout when using automatic layout (default: 4). |
figsize |
Optional[Tuple[float, float]] |
Size of the combined figure (width, height) in inches. If None, will be calculated based on input figures. |
sharex |
bool |
Whether to share the x-axis across all subplots (default: False). |
sharey |
bool |
Whether to share the y-axis across all subplots (default: False). |
For more details, see the datachart.utils.FigureGridLayout function documentation.
Basic Grid Layout¶
The simplest way to combine charts is to create individual charts and then combine them into a grid. The figure_grid_layout function automatically arranges them in a grid layout.
Comparing Two Experimental Conditions¶
Let's start with a simple example comparing two different experimental treatments:
# Create two individual line charts showing bacterial growth curves
# Control group - normal growth
fig1 = LineChart(
data=[{"x": i, "y": 0.1 * np.exp(0.3 * i)} for i in range(10)],
title="Control Group",
subtitle="Untreated E. coli",
xlabel="Time (hours)",
ylabel="OD600 (Optical Density)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Antibiotic-treated group - inhibited growth
fig2 = LineChart(
data=[{"x": i, "y": 0.1 * np.exp(0.12 * i)} for i in range(10)],
title="Antibiotic Treatment",
subtitle="50 μg/mL Ampicillin",
xlabel="Time (hours)",
ylabel="OD600 (Optical Density)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Combine into a grid using FigureGridLayout
combined = FigureGridLayout(
charts=[
{"figure": fig1},
{"figure": fig2},
],
title="Bacterial Growth Kinetics Comparison",
max_cols=2
)
Comparing Multiple Treatment Groups¶
You can combine any number of charts. The function automatically calculates the grid layout based on the max_cols parameter:
# Create multiple charts showing different drug candidates
# Each chart shows dose-response relationship
fig1 = LineChart(
data=[
[{"x": i, "y": 100 - 95 / (1 + np.exp(-(i - 5))) + np.random.randn() * 2} for i in range(11)],
],
subtitle="Compound A",
xlabel="Concentration (μM)",
ylabel="Cell Viability (%)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
fig2 = LineChart(
data=[{"x": i, "y": 100 - 85 / (1 + np.exp(-(i - 6))) + np.random.randn() * 2} for i in range(11)],
subtitle="Compound B",
xlabel="Concentration (μM)",
ylabel="Cell Viability (%)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
fig3 = LineChart(
data=[{"x": i, "y": 100 - 92 / (1 + np.exp(-(i - 4))) + np.random.randn() * 2} for i in range(11)],
subtitle="Compound C",
xlabel="Concentration (μM)",
ylabel="Cell Viability (%)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
fig4 = LineChart(
data=[{"x": i, "y": 100 - 78 / (1 + np.exp(-(i - 7))) + np.random.randn() * 2} for i in range(11)],
subtitle="Compound D",
xlabel="Concentration (μM)",
ylabel="Cell Viability (%)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Combine into a 2x2 grid
combined = FigureGridLayout(
charts=[
{"figure": fig1},
{"figure": fig2},
{"figure": fig3},
{"figure": fig4},
],
title="Drug Candidate Screening: Dose-Response Curves",
max_cols=2
)
# Create 6 charts showing temperature readings from different sensors
np.random.seed(42)
charts = []
sensor_locations = ["Lab A", "Lab B", "Lab C", "Incubator 1", "Incubator 2", "Cold Room"]
target_temps = [22, 22, 22, 37, 37, 4]
for i, (location, target) in enumerate(zip(sensor_locations, target_temps)):
# Generate temperature data with small fluctuations
temp_data = [{"x": j, "y": target + np.random.randn() * 0.5} for j in range(24)]
fig = LineChart(
data=temp_data,
subtitle=f"{location} (Target: {target}°C)",
xlabel="Time (hours)",
ylabel="Temperature (°C)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
charts.append({"figure": fig})
# Combine with max_cols=3 (creates a 2x3 grid)
combined = FigureGridLayout(
charts=charts,
title="Environmental Monitoring: 24-Hour Temperature Log",
max_cols=3
)
Custom Figure Size¶
You can specify a custom figure size for the combined chart:
# Compare two different visualization types for protein expression data
np.random.seed(42)
# Line chart showing expression over time
time_points = range(8)
expression_levels = [0.2, 0.5, 1.2, 2.8, 4.5, 5.2, 5.0, 4.8]
fig1 = LineChart(
data=[{"x": t, "y": expr} for t, expr in zip(time_points, expression_levels)],
subtitle="Temporal Expression Profile",
xlabel="Time (hours post-induction)",
ylabel="Relative Expression",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Bar chart comparing final expression across different cell lines
cell_lines = ["HEK293", "CHO-K1", "HeLa", "NIH-3T3"]
final_expression = [4.8, 6.2, 3.9, 5.5]
fig2 = BarChart(
data=[{"label": cell_lines[i], "y": final_expression[i]} for i in range(len(cell_lines))],
subtitle="Expression by Cell Line (8h)",
xlabel="Cell Line",
ylabel="Relative Expression",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Combine with custom figure size
combined = FigureGridLayout(
charts=[
{"figure": fig1},
{"figure": fig2},
],
title="Protein Expression Analysis",
max_cols=2,
figsize=FIG_SIZE.A4_NARROW
)
Custom Layout Specs¶
You can specify a custom layout specs for the combined chart to create asymmetric layouts:
# Create a comprehensive view with one main chart and two supporting charts
np.random.seed(42)
# Main chart: Full spectroscopic scan
wavelengths = np.linspace(200, 800, 100)
absorbance = 0.8 * np.exp(-0.5 * ((wavelengths - 450) / 80)**2) + \
0.3 * np.exp(-0.5 * ((wavelengths - 280) / 40)**2)
fig1 = LineChart(
data=[{"x": w, "y": a + np.random.randn() * 0.02} for w, a in zip(wavelengths, absorbance)],
subtitle="UV-Vis Absorption Spectrum",
xlabel="Wavelength (nm)",
ylabel="Absorbance (AU)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Supporting chart 1: Peak analysis
peaks = ["Peak 1\n(280nm)", "Peak 2\n(450nm)"]
peak_heights = [0.32, 0.81]
fig2 = BarChart(
data=[{"label": peaks[i], "y": peak_heights[i]} for i in range(len(peaks))],
subtitle="Peak Intensity",
xlabel="Peak",
ylabel="Absorbance (AU)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Supporting chart 2: Concentration calibration
concentrations = [0, 10, 25, 50, 75, 100]
abs_values = [0.05, 0.21, 0.48, 0.95, 1.42, 1.88]
fig3 = ScatterChart(
data=[{"x": c, "y": a} for c, a in zip(concentrations, abs_values)],
subtitle="Calibration Curve",
xlabel="Concentration (μg/mL)",
ylabel="Absorbance at 450nm",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Use custom layout: main chart on top, two smaller charts below
combined = FigureGridLayout(
charts=[
{"figure": fig1, "layout_spec": {"row": 0, "col": 0, "rowspan": 1, "colspan": 2}},
{"figure": fig2, "layout_spec": {"row": 1, "col": 0, "rowspan": 1, "colspan": 1}},
{"figure": fig3, "layout_spec": {"row": 1, "col": 1, "rowspan": 1, "colspan": 1}},
],
title="Spectrophotometric Analysis Dashboard",
figsize=FIG_SIZE.A4_REGULAR
)
# Create charts showing different physiological parameters over the same time period
np.random.seed(42)
time_hours = range(24)
# Heart rate data
baseline_hr = 72
hr_data = [{"x": i, "y": baseline_hr + 8 * np.sin(i * np.pi / 12) + np.random.randn() * 3}
for i in time_hours]
fig1 = LineChart(
data=hr_data,
subtitle="Heart Rate",
xlabel="Time (hours)",
ylabel="BPM",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Blood pressure (systolic) data
baseline_bp = 120
bp_data = [{"x": i, "y": baseline_bp + 5 * np.sin(i * np.pi / 12) + np.random.randn() * 4}
for i in time_hours]
fig2 = LineChart(
data=bp_data,
subtitle="Systolic Blood Pressure",
xlabel="Time (hours)",
ylabel="mmHg",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Body temperature data
baseline_temp = 37.0
temp_data = [{"x": i, "y": baseline_temp + 0.3 * np.sin((i - 4) * np.pi / 12) + np.random.randn() * 0.1}
for i in time_hours]
fig3 = LineChart(
data=temp_data,
subtitle="Core Body Temperature",
xlabel="Time (hours)",
ylabel="°C",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Combine with shared x-axis
combined = FigureGridLayout(
charts=[
{"figure": fig1},
{"figure": fig2},
{"figure": fig3},
],
title="Continuous Patient Monitoring (24-Hour Period)",
max_cols=3,
sharex=True
)
Sharing Y-Axis¶
Use sharey=True to share the y-axis across all charts. This is ideal when comparing similar measurements across different conditions:
# Create charts comparing enzyme activity under different pH conditions
# All charts measure the same metric (activity) so sharing y-axis makes comparison easier
np.random.seed(42)
substrates = ["Substrate A", "Substrate B", "Substrate C", "Substrate D"]
# pH 5.0 condition
activity_ph5 = [35, 42, 38, 40]
fig1 = BarChart(
data=[{"label": substrates[i], "y": activity_ph5[i] + np.random.randn() * 2} for i in range(4)],
subtitle="pH 5.0",
xlabel="Substrate",
ylabel="Activity (U/mg)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# pH 7.0 condition (optimal)
activity_ph7 = [85, 92, 88, 90]
fig2 = BarChart(
data=[{"label": substrates[i], "y": activity_ph7[i] + np.random.randn() * 3} for i in range(4)],
subtitle="pH 7.0 (Optimal)",
xlabel="Substrate",
ylabel="Activity (U/mg)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# pH 9.0 condition
activity_ph9 = [52, 58, 55, 56]
fig3 = BarChart(
data=[{"label": substrates[i], "y": activity_ph9[i] + np.random.randn() * 2} for i in range(4)],
subtitle="pH 9.0",
xlabel="Substrate",
ylabel="Activity (U/mg)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Combine with shared y-axis for easy comparison
combined = FigureGridLayout(
charts=[
{"figure": fig1},
{"figure": fig2},
{"figure": fig3},
],
title="Enzyme Activity Assay: pH Optimization Study",
max_cols=3,
sharey=True
)
Sharing Both Axes¶
You can share both x and y axes for direct comparison of datasets with identical scales:
# Create scatter charts comparing gene expression correlations in different tissues
# All plots show same type of correlation (Gene A vs Gene B expression)
np.random.seed(42)
# Liver tissue
liver_data = [{"x": i + np.random.randn() * 2,
"y": i * 1.8 + 5 + np.random.randn() * 3} for i in range(20)]
fig1 = ScatterChart(
data=liver_data,
subtitle="Liver Tissue (r=0.89)",
xlabel="Gene A Expression (FPKM)",
ylabel="Gene B Expression (FPKM)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Brain tissue
brain_data = [{"x": i + np.random.randn() * 2,
"y": i * 1.7 + 3 + np.random.randn() * 4} for i in range(20)]
fig2 = ScatterChart(
data=brain_data,
subtitle="Brain Tissue (r=0.85)",
xlabel="Gene A Expression (FPKM)",
ylabel="Gene B Expression (FPKM)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Muscle tissue
muscle_data = [{"x": i + np.random.randn() * 2,
"y": i * 1.9 + 2 + np.random.randn() * 3} for i in range(20)]
fig3 = ScatterChart(
data=muscle_data,
subtitle="Muscle Tissue (r=0.92)",
xlabel="Gene A Expression (FPKM)",
ylabel="Gene B Expression (FPKM)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Combine with shared axes for direct comparison
combined = FigureGridLayout(
charts=[
{"figure": fig1},
{"figure": fig2},
{"figure": fig3},
],
title="Gene Co-Expression Analysis Across Tissues",
max_cols=3,
sharex=True,
sharey=True
)
Mixing Different Chart Types¶
One of the powerful features of figure_grid_layout is that you can combine different chart types in the same grid for comprehensive analysis:
# Create different chart types for analyzing chromatography results
np.random.seed(42)
# Line chart: Chromatogram (absorbance vs time)
retention_times = np.linspace(0, 30, 150)
# Create three peaks at different retention times
peak1 = 0.4 * np.exp(-0.5 * ((retention_times - 8) / 1.2)**2)
peak2 = 0.8 * np.exp(-0.5 * ((retention_times - 15) / 1.5)**2)
peak3 = 0.3 * np.exp(-0.5 * ((retention_times - 22) / 1.0)**2)
absorbance = peak1 + peak2 + peak3 + np.random.randn(150) * 0.02
line_fig = LineChart(
data=[{"x": t, "y": max(0, a)} for t, a in zip(retention_times, absorbance)],
subtitle="Chromatogram",
xlabel="Retention Time (min)",
ylabel="Absorbance (AU)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Bar chart: Peak areas (quantification)
peaks = ["Peak 1\n(8 min)", "Peak 2\n(15 min)", "Peak 3\n(22 min)"]
areas = [1250, 2890, 920]
bar_fig = BarChart(
data=[{"label": peaks[i], "y": areas[i]} for i in range(3)],
subtitle="Peak Integration",
xlabel="Compound",
ylabel="Area (mAU·min)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Scatter chart: Calibration curve
standards = [0, 5, 10, 25, 50, 100] # μg/mL
peak_areas = [0, 145, 298, 720, 1455, 2920]
scatter_fig = ScatterChart(
data=[{"x": c, "y": a + np.random.randn() * 30} for c, a in zip(standards, peak_areas)],
subtitle="Standard Curve",
xlabel="Concentration (μg/mL)",
ylabel="Peak Area (mAU·min)",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
# Combine different chart types
combined = FigureGridLayout(
charts=[
{"figure": line_fig},
{"figure": bar_fig},
{"figure": scatter_fig},
],
title="HPLC Analysis: Quantitative Results",
max_cols=3
)
Complete Example: Multi-Site Clinical Trial Dashboard¶
Here's a complete example creating a dashboard with multiple related visualizations for a clinical trial:
# Generate clinical trial data across multiple sites
np.random.seed(42)
weeks = list(range(12))
sites = ["Site A (NY)", "Site B (LA)", "Site C (Chicago)", "Site D (Boston)"]
# Patient recruitment trends
recruitment_data = {
"Site A (NY)": [5, 12, 18, 25, 31, 38, 42, 45, 48, 50, 52, 53],
"Site B (LA)": [3, 8, 15, 21, 28, 34, 39, 43, 46, 48, 50, 51],
"Site C (Chicago)": [4, 10, 16, 23, 29, 35, 40, 44, 47, 49, 51, 52],
"Site D (Boston)": [2, 7, 13, 19, 26, 32, 37, 41, 44, 46, 48, 49],
}
# Create individual charts for each site
charts = []
for site in sites:
fig = LineChart(
data=[{"x": w, "y": recruitment_data[site][w]} for w in weeks],
subtitle=site,
xlabel="Week",
ylabel="Cumulative Patients",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
charts.append({"figure": fig})
# Add a summary chart showing all sites together
all_sites_data = []
for site in sites:
all_sites_data.append([{"x": w, "y": recruitment_data[site][w]} for w in weeks])
summary_fig = LineChart(
data=all_sites_data,
subtitle="All Sites Combined",
xlabel="Week",
ylabel="Cumulative Patients",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
charts.append({"figure": summary_fig})
# Add adverse events bar chart
adverse_events = [8, 6, 7, 5]
ae_fig = BarChart(
data=[{"label": sites[i], "y": adverse_events[i]} for i in range(len(sites))],
subtitle="Adverse Events Reported",
xlabel="Site",
ylabel="Count",
figsize=FIG_SIZE.A4_HALF_NARROW,
)
charts.append({"figure": ae_fig})
# Combine into dashboard
dashboard = FigureGridLayout(
charts=charts,
title="Multi-Site Clinical Trial: Enrollment & Safety Monitoring",
max_cols=3,
figsize=FIG_SIZE.A4_NARROW
)
Notes¶
- Each chart figure must be created using a datachart chart function (e.g.,
LineChart,BarChart,ScatterChart) to have the required metadata. - The function automatically calculates the number of rows based on the number of figures and
max_cols. - Unused grid positions are automatically hidden.
- Individual chart titles (from the
titleparameter) are used as subtitles in the grid, while thetitleparameter ofFigureGridLayoutbecomes the overall figure title. - Chart-specific labels (xlabel, ylabel, subtitle) from individual charts are preserved in the grid layout.
- For custom layouts, all charts must have a
layout_specdictionary specifyingrow,col,rowspan, andcolspan.