Composition¶
Composition combines complete views into a larger visualization. GenomeSpy uses three concise operators for the most common layouts:
Operator |
Composition |
Result |
|---|---|---|
|
Layer |
Draw |
|
Vertical concatenation |
Place |
|
Horizontal concatenation |
Place |
Use concat() for a grid,
facet() to repeat a plot by category, and
surround() to place plots around a main chart.
Layer complementary marks¶
Layer marks when they describe the same coordinate system. The first layer is drawn first; later layers appear above it:
import genome_spy as gs
measurements = [
{"position": 1, "signal": 2.1, "quality": 0.72, "group": "control", "label": "C1"},
{"position": 2, "signal": 2.8, "quality": 0.84, "group": "control", "label": "C2"},
{"position": 3, "signal": 3.2, "quality": 0.66, "group": "control", "label": "C3"},
{"position": 1, "signal": 2.4, "quality": 0.78, "group": "treated", "label": "T1"},
{"position": 2, "signal": 3.5, "quality": 0.91, "group": "treated", "label": "T2"},
{"position": 3, "signal": 4.3, "quality": 0.88, "group": "treated", "label": "T3"},
]
points = gs.Chart().mark_point(filled=True, size=100)
labels = gs.Chart().mark_text(dy=-12).encode(text="label:N")
layered_chart = (
(points + labels)
.encode(
x=gs.X("position:Q").title("Position"),
y=gs.Y("signal:Q").scale(zero=False).title("Signal"),
color=gs.Color("group:N").legend(title="Group"),
)
.properties(data=measurements, title="Points with labels")
)
Neither child declares data, positions, or color. Those properties belong to
the layered parent and are inherited by both children. The text layer adds
only the text encoding that it alone needs.
Place shared properties at the nearest common parent. Descendants can override an inherited data source, encoding, transform, or view property when their behavior differs. See layering views for the layer-specific options.
Stack aligned tracks vertically¶
The & operator gives each child its own plot area. It is particularly useful
for aligned tracks that share horizontal positions:
signal_track = (
gs.Chart()
.mark_point(filled=True, size=90)
.encode(
y=gs.Y("signal:Q").scale(zero=False).title("Signal"),
color="group:N",
)
.properties(height=150)
.with_view(stroke="lightgray")
)
quality_track = (
gs.Chart()
.mark_point(filled=True, size=90, color="#6f6f6f")
.encode(y=gs.Y("quality:Q").scale(domain=[0, 1]).title("Quality"))
.properties(height=90)
.with_view(stroke="lightgray")
)
vertical_chart = (
(signal_track & quality_track)
.encode(x=gs.X("position:Q").scale(domain=[0.5, 3.5], zoom=True))
.properties(data=measurements, spacing=20, title="Two aligned tracks")
.resolve_scale(x="shared", y="independent")
.resolve_axis(x="shared", y="independent")
)
The parent supplies the data and x encoding. Both tracks therefore use one
zoomable horizontal scale. Their y scales remain independent because signal
and quality have different units and domains.
The light-gray view strokes and 20-pixel gap make the two schematic child views easy to distinguish while reading the composition.
Vertical concatenation shares x resolution by default because aligned tracks
are common in GenomeSpy. The explicit calls above document the intended
relationship and keep it visible when the chart becomes more complex.
Compare panels side by side¶
The | operator keeps panels separate while arranging them horizontally:
control_panel = (
gs.Chart()
.transform_filter(gs.datum.group == "control")
.mark_point(filled=True, size=100, color="#4c78a8")
.encode(x="position:Q", y=gs.Y("signal:Q").scale(zero=False))
.properties(title="Control")
.with_view(stroke="lightgray")
)
treated_panel = (
gs.Chart()
.transform_filter(gs.datum.group == "treated")
.mark_point(filled=True, size=100, color="#e45756")
.encode(x="position:Q", y=gs.Y("signal:Q").scale(zero=False))
.properties(title="Treated")
.with_view(stroke="lightgray")
)
horizontal_chart = (
(control_panel | treated_panel)
.properties(data=measurements, spacing=20)
.resolve_scale(x="shared", y="shared")
.resolve_axis(x="independent", y="shared")
)
The children filter the inherited data independently. Shared x and y
scales make positions directly comparable, while independent x axes let each
panel label that shared mapping within its own column. Child sizing, spacing,
and separators are documented in
view concatenation.
Repeat categories with facets¶
Use facet() to repeat one plot by category. The container
supplies the data; Core splits the records and runs the child plot’s transforms
for each category. Facets share scales and axes. FacetFieldDef specifies a
single field for a wrapped layout; FacetMapping specifies rows and columns.
facet_chart = gs.facet(
gs.Chart().mark_point(filled=True, size=90).encode(x="position:Q", y="signal:Q"),
facet=gs.FacetMapping().column(
field="group",
title=gs.Title(text="group", reserve=False, offset=19, baseline="middle"),
header=gs.FacetHeader(fontSize=13, color="#444444"),
),
data=measurements,
spacing=gs.FacetSpacing(column=18),
)
Header, spacing and field options use named Python arguments. See the upstream facet guide for layout constraints.
Add plots with surround¶
Use surround() to add plots above, below, or beside a main
chart. Pass the main chart first. Choose sides with
SurroundSides:
surround_chart = (
gs.surround(
gs.Chart()
.mark_point(filled=True, size=90)
.encode(x="position:I", y="signal:Q"),
surround=gs.SurroundSides(
top=[
gs.Chart()
.transform_aggregate(
groupby=["position"],
fields=["signal"],
ops=["mean"],
as_=["meanSignal"],
)
.mark_rect()
.encode(
x=gs.X("position:I").axis(None),
y=gs.Y("meanSignal:Q").axis(title="Mean signal"),
tooltip=["position:I", gs.Tooltip("meanSignal:Q", format=".2f")],
)
.properties(height=80)
]
),
data=measurements,
spacing=8,
)
.with_scales(x=gs.Scale(domain=[1, 3], paddingInner=0.25, zoom=False))
.configure_scale(zoom=False)
)
The bars show average signal at each position. GenomeSpy calculates the averages in the browser. Bars line up with the points below.
Side |
Aligns with |
Set each side plot’s |
|---|---|---|
|
Main chart’s x positions |
|
|
Main chart’s y positions |
|
Set the main chart’s width and height to size its plot area. Use spacing
to set the gap around it. Each side takes a list of charts.
For several plots on one side, gs.SurroundSideSpec(views=[...], spacing=4)
sets their gap. It can also give them shared data or settings.
See the surround reference
for nested layouts and supported chart types.
Add row and column totals¶
This UpSet-style example uses gs.surround() to place bars above and beside a
matrix. The fictional data includes the totals. GenomeSpy filters repeated
totals and draws the charts in the browser.
memberships = [
{"column": 0, "row": 0, "member": True, "columnTotal": 2, "rowTotal": 2},
{"column": 0, "row": 1, "member": True, "columnTotal": 2, "rowTotal": 2},
{"column": 0, "row": 2, "member": False, "columnTotal": 2, "rowTotal": 2},
{"column": 1, "row": 0, "member": True, "columnTotal": 3, "rowTotal": 2},
{"column": 1, "row": 1, "member": True, "columnTotal": 3, "rowTotal": 2},
{"column": 1, "row": 2, "member": True, "columnTotal": 3, "rowTotal": 2},
{"column": 2, "row": 0, "member": False, "columnTotal": 1, "rowTotal": 2},
{"column": 2, "row": 1, "member": False, "columnTotal": 1, "rowTotal": 2},
{"column": 2, "row": 2, "member": True, "columnTotal": 1, "rowTotal": 2},
]
column_bars = (
gs.Chart()
.mark_rect(color="#555")
.encode(
x=gs.X("column:I").axis(None),
y=gs.Y("columnTotal:Q").axis(title="Column total", tickMinStep=1),
)
)
column_labels = (
gs.Chart()
.mark_text(dy=-8)
.encode(
x="column:I",
y="columnTotal:Q",
text="columnTotal:Q",
)
)
column_summary = (
(column_bars + column_labels)
.transform_filter(gs.datum.row == 0)
.properties(height=90)
)
row_bars = (
gs.Chart()
.mark_rect(color="#777")
.encode(
x=gs.X("rowTotal:Q").axis(title="Row total", tickMinStep=1),
y=gs.Y("row:I").axis(None),
)
)
row_labels = (
gs.Chart()
.mark_text(align="right", dx=-5, color="white")
.encode(
x="rowTotal:Q",
y="row:I",
text="rowTotal:Q",
)
)
row_summary = (
(row_bars + row_labels).transform_filter(gs.datum.column == 0).properties(width=110)
)
matrix = (
gs.Chart()
.mark_point(filled=True, size=180)
.encode(
x=gs.X("column:I").axis(None),
y=gs.Y("row:I").axis(None),
color=gs.Color("member:N")
.scale(domain=[False, True], range=["#d8d8d8", "#333333"])
.legend(None),
)
.properties(width="container", height="container")
)
matrix_summary_chart = (
gs.surround(
matrix,
surround=gs.SurroundSides(top=[column_summary], left=[row_summary]),
data=memberships,
spacing=8,
title="Aligned summaries and matrix",
)
.with_scales(
x=gs.Scale(domain=[0, 2], paddingInner=0.15, paddingOuter=0.1, zoom=False),
y=gs.Scale(domain=[0, 2], paddingInner=0.15, paddingOuter=0.1, zoom=False),
)
.configure_scale(zoom=False)
)
Top bars follow the matrix columns. Left bars follow its rows. Each bar chart has its own count scale.
See the UpSet gallery example for a complete plot using mutation data.