π PlottingΒΆ
awpy.plot draws demo data on a mapβs radar image β the same 1024Γ1024
PNGs used by the in-game radar, fetched on demand by awpy.data. It needs
matplotlib, which ships as an optional extra:
pip install 'awpy[plot]'
Every function takes a map name, and β like the rest of awpy β an optional
version= to pin an awpy-data
release (an integer ClientVersion); by default the newest cached release is
used, downloading the latest only when nothing is cached.
Game state: frameΒΆ
A frame is one moment of a round: players, and optionally the bomb. Each
player is a plot.Player β only x/y are required, everything else adds
detail:
from awpy import plot
players = [
plot.Player(x=-158.6, y=819.1, z=103.7, side="ct", hp=87, armor=50, label="ct1"),
plot.Player(x=1258.0, y=455.5, z=181.2, side="t", yaw=135.0),
plot.Player(x=800.0, y=600.0, z=150.0, side="t", hp=0), # dead -> dim cross
]
fig, ax = plot.frame("de_inferno", players, bomb=(200.0, 480.0, 90.0))
fig.savefig("frame.png")
sidecolors the marker β"t"/"ct", or the demo dataframesβ"terrorist"/"counter-terrorist", any case.color=overrides it.hp/armordraw bars under the marker;hp=0renders the player dead.yaw(degrees, 0 = +x, counter-clockwise β the demoβs eye-angle yaw) draws a view-direction tick.labelprints text below the marker.
demo.snapshots(ticks=tick) produces exactly these fields, so any moment of a real
demo is one comprehension away:
from awpy import Demo, plot
demo = Demo("match.dem")
snap = demo.snapshots(ticks=29000)
players = [
plot.Player(x=r["x"], y=r["y"], z=r["z"], yaw=r["yaw"], hp=r["health"],
armor=r["armor"], side=r["side"], label=r["name"])
for r in snap.iter_rows(named=True)
]
fig, ax = plot.frame(demo.header["map_name"], players)
Heatmaps: heatmapΒΆ
Pass any iterable of world (x, y) or (x, y, z) rows β with Polars,
.rows() on a selection does it:
from awpy import Demo, plot
kills = Demo("match.dem").kills
deaths = kills.select(["victim_x", "victim_y", "victim_z"]).rows()
fig, ax = plot.heatmap("de_inferno", deaths, method="kde")
fig, ax = plot.heatmap("de_inferno", deaths, method="hex", bins=36)
fig, ax = plot.heatmap("de_inferno", deaths, method="hist", bins=64, cmap="viridis")
method is "hex" (hexagonal binning), "hist" (square binning), or
"kde" (gaussian-smoothed density; tune the smoothing radius in radar pixels
with bandwidth=). bins sets the grid resolution, cmap any matplotlib
colormap, alpha the overlay opacity. Empty cells stay transparent, so the
radar shows through.
Multi-level mapsΒΆ
Nuke, Vertigo, Train, and Baggage have two radar images. Pass lower=True to
draw the lower level; points are matched to their level by altitude, using
the vertical sections published in map_data.json:
fig, ax = plot.frame("de_nuke", players, lower=True) # ramp room & B site
plot.is_lower_level("de_nuke", z=-600.0) # -> True
plot.has_lower_level("de_nuke") # -> True
On a single radar, players on the other level are dimmed (off_level_alpha=,
0 hides them) and off-level heatmap points are skipped with a warning. When a
game state spans both levels, use the _levels variants β one panel per level,
everything drawn at full opacity on the level itβs on:
fig, (upper, lower) = plot.frame_levels("de_nuke", players, bomb=bomb)
fig, axes = plot.heatmap_levels("de_nuke", deaths, method="kde")
Both return (fig, (ax_upper, ax_lower)), and degrade gracefully to a single
panel on single-level maps β no need to special-case the map.
Animation: gifΒΆ
Render a sequence of frames to an animated GIF β each entry is a list of
players, or a dict of frame keyword arguments:
plot.gif(
"de_inferno",
[
[plot.Player(x=0, y=0, side="t")],
{"players": [plot.Player(x=50, y=20, side="t")], "bomb": (200, 480, 90)},
],
"round.gif",
fps=2,
)
Building blocksΒΆ
radar draws the bare map and returns (fig, ax); every other function
accepts ax= so you can layer plots β e.g. a heatmap under a frame, or your
own matplotlib artists on top. Positions are drawn in radar pixel
coordinates, and the transforms are public:
fig, ax = plot.radar("de_inferno", version=2000873)
plot.heatmap("de_inferno", deaths, method="kde", ax=ax)
plot.frame("de_inferno", players, ax=ax)
px, py = plot.world_to_pixel("de_inferno", (1258.0, 455.5))
x, y = plot.pixel_to_world("de_inferno", (px, py))
All of it is Hammer units, Z-up, straight from the demo dataframes β the same
frame as VisibilityChecker.