|
1 | 1 | """ pyplots.ai |
2 | 2 | bump-basic: Basic Bump Chart |
3 | | -Library: plotnine 0.15.2 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-23 |
| 3 | +Library: plotnine 0.15.3 | Python 3.14.3 |
| 4 | +Quality: 94/100 | Updated: 2026-02-22 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import pandas as pd |
8 | 8 | from plotnine import ( |
9 | 9 | aes, |
| 10 | + element_blank, |
| 11 | + element_line, |
| 12 | + element_rect, |
10 | 13 | element_text, |
11 | 14 | geom_line, |
12 | 15 | geom_point, |
13 | 16 | geom_text, |
14 | 17 | ggplot, |
15 | 18 | labs, |
16 | | - scale_color_brewer, |
| 19 | + scale_color_manual, |
17 | 20 | scale_x_continuous, |
18 | 21 | scale_y_reverse, |
19 | 22 | theme, |
20 | 23 | theme_minimal, |
21 | 24 | ) |
22 | 25 |
|
23 | 26 |
|
24 | | -# Data - Tech company rankings over 6 quarters |
25 | | -data = { |
26 | | - "entity": ["Alpha Corp"] * 6 + ["Beta Inc"] * 6 + ["Gamma Tech"] * 6 + ["Delta Systems"] * 6 + ["Epsilon Labs"] * 6, |
27 | | - "period": ["Q1", "Q2", "Q3", "Q4", "Q5", "Q6"] * 5, |
28 | | - "period_num": [1, 2, 3, 4, 5, 6] * 5, |
29 | | - "rank": [ |
30 | | - 1, |
31 | | - 1, |
32 | | - 2, |
33 | | - 2, |
34 | | - 1, |
35 | | - 1, # Alpha Corp - starts strong, slight dip, recovers |
36 | | - 2, |
37 | | - 3, |
38 | | - 1, |
39 | | - 1, |
40 | | - 2, |
41 | | - 3, # Beta Inc - rises to top mid-year, then falls |
42 | | - 3, |
43 | | - 2, |
44 | | - 3, |
45 | | - 4, |
46 | | - 4, |
47 | | - 2, # Gamma Tech - volatile movement |
48 | | - 4, |
49 | | - 4, |
50 | | - 4, |
51 | | - 3, |
52 | | - 3, |
53 | | - 4, # Delta Systems - stable middle performer |
54 | | - 5, |
55 | | - 5, |
56 | | - 5, |
57 | | - 5, |
58 | | - 5, |
59 | | - 5, # Epsilon Labs - consistently last |
60 | | - ], |
| 27 | +# Data - Streaming platform market share rankings over 8 quarters |
| 28 | +platforms = ["StreamVue", "WavePlay", "CloudCast", "PixelFlix", "SonicNet", "EchoTV"] |
| 29 | +quarters = ["Q1'24", "Q2'24", "Q3'24", "Q4'24", "Q1'25", "Q2'25", "Q3'25", "Q4'25"] |
| 30 | +n_periods = len(quarters) |
| 31 | + |
| 32 | +rankings = { |
| 33 | + "StreamVue": [1, 1, 1, 2, 2, 3, 3, 4], |
| 34 | + "WavePlay": [2, 3, 3, 1, 1, 1, 1, 1], |
| 35 | + "CloudCast": [4, 2, 2, 3, 3, 2, 2, 2], |
| 36 | + "PixelFlix": [3, 4, 4, 4, 5, 5, 4, 3], |
| 37 | + "SonicNet": [5, 5, 5, 5, 4, 4, 5, 5], |
| 38 | + "EchoTV": [6, 6, 6, 6, 6, 6, 6, 6], |
61 | 39 | } |
62 | | -df = pd.DataFrame(data) |
| 40 | + |
| 41 | +rows = [] |
| 42 | +for platform, ranks in rankings.items(): |
| 43 | + for i, rank in enumerate(ranks): |
| 44 | + rows.append({"platform": platform, "quarter": quarters[i], "qnum": i + 1, "rank": rank}) |
| 45 | +df = pd.DataFrame(rows) |
63 | 46 |
|
64 | 47 | # Subset for end labels |
65 | | -df_end = df[df["period_num"] == 6].copy() |
| 48 | +df_end = df[df["qnum"] == n_periods].copy() |
| 49 | + |
| 50 | +# Visual hierarchy: protagonist entities vs supporting cast |
| 51 | +protagonists = ["StreamVue", "WavePlay"] |
| 52 | +supporting = ["CloudCast", "PixelFlix", "SonicNet", "EchoTV"] |
| 53 | + |
| 54 | +df_hero = df[df["platform"].isin(protagonists)] |
| 55 | +df_support = df[df["platform"].isin(supporting)] |
| 56 | + |
| 57 | +# Crossover emphasis at Q4'24 (qnum=4) where WavePlay overtakes StreamVue |
| 58 | +df_crossover = pd.DataFrame( |
| 59 | + [{"qnum": 4, "rank": 1, "platform": "WavePlay"}, {"qnum": 4, "rank": 2, "platform": "StreamVue"}] |
| 60 | +) |
| 61 | + |
| 62 | +# Colorblind-safe palette — Python Blue first, warm orange for WavePlay |
| 63 | +# Replaced red with teal (#17becf) for deuteranopia safety |
| 64 | +palette = { |
| 65 | + "StreamVue": "#306998", |
| 66 | + "WavePlay": "#e8963e", |
| 67 | + "CloudCast": "#59a14f", |
| 68 | + "PixelFlix": "#17becf", |
| 69 | + "SonicNet": "#9d7660", |
| 70 | + "EchoTV": "#bab0ac", |
| 71 | +} |
66 | 72 |
|
67 | | -# Plot |
| 73 | +# Plot — layered for visual hierarchy |
68 | 74 | plot = ( |
69 | | - ggplot(df, aes(x="period_num", y="rank", color="entity", group="entity")) |
70 | | - + geom_line(size=2.5, alpha=0.8) |
71 | | - + geom_point(size=6) |
72 | | - + geom_text(aes(label="entity"), data=df_end, nudge_x=0.3, ha="left", size=12) |
73 | | - + scale_y_reverse(breaks=[1, 2, 3, 4, 5]) |
74 | | - + scale_x_continuous(breaks=[1, 2, 3, 4, 5, 6], labels=["Q1", "Q2", "Q3", "Q4", "Q5", "Q6"], limits=(0.5, 7.5)) |
75 | | - + scale_color_brewer(type="qual", palette="Set2") |
76 | | - + labs(x="Quarter", y="Rank", title="bump-basic · plotnine · pyplots.ai", color="Company") |
| 75 | + ggplot(df, aes(x="qnum", y="rank", color="platform", group="platform")) |
| 76 | + # Supporting lines: thinner, more transparent |
| 77 | + + geom_line(data=df_support, size=1.8, alpha=0.4) |
| 78 | + + geom_point(data=df_support, size=4, stroke=0.6, fill="white") |
| 79 | + + geom_point(data=df_support, size=2.5, alpha=0.5) |
| 80 | + # Protagonist lines: bold and saturated |
| 81 | + + geom_line(data=df_hero, size=3.5, alpha=0.95) |
| 82 | + + geom_point(data=df_hero, size=7, stroke=1.0, fill="white") |
| 83 | + + geom_point(data=df_hero, size=4.5) |
| 84 | + # Crossover emphasis at Q4'24 |
| 85 | + + geom_point(data=df_crossover, size=12, alpha=0.15) |
| 86 | + # End labels — bold for protagonists, italic for supporting |
| 87 | + + geom_text( |
| 88 | + aes(label="platform"), |
| 89 | + data=df_end[df_end["platform"].isin(protagonists)], |
| 90 | + nudge_x=0.35, |
| 91 | + ha="left", |
| 92 | + size=13, |
| 93 | + fontweight="bold", |
| 94 | + ) |
| 95 | + + geom_text( |
| 96 | + aes(label="platform"), |
| 97 | + data=df_end[df_end["platform"].isin(supporting)], |
| 98 | + nudge_x=0.35, |
| 99 | + ha="left", |
| 100 | + size=11, |
| 101 | + fontstyle="italic", |
| 102 | + alpha=0.7, |
| 103 | + ) |
| 104 | + + scale_y_reverse(breaks=range(1, len(platforms) + 1)) |
| 105 | + + scale_x_continuous(breaks=range(1, n_periods + 1), labels=quarters, limits=(0.5, n_periods + 2)) |
| 106 | + + scale_color_manual(values=palette) |
| 107 | + + labs(x="Quarter", y="Market Share Ranking", title="bump-basic · plotnine · pyplots.ai") |
77 | 108 | + theme_minimal() |
78 | 109 | + theme( |
79 | 110 | figure_size=(16, 9), |
80 | | - text=element_text(size=14), |
81 | | - axis_title=element_text(size=20), |
82 | | - axis_text=element_text(size=16), |
83 | | - plot_title=element_text(size=24), |
84 | | - legend_text=element_text(size=16), |
85 | | - legend_title=element_text(size=18), |
| 111 | + text=element_text(size=14, color="#3c3c3c"), |
| 112 | + axis_title=element_text(size=20, color="#555555"), |
| 113 | + axis_text=element_text(size=16, color="#666666"), |
| 114 | + axis_text_x=element_text(rotation=0), |
| 115 | + plot_title=element_text(size=24, weight="bold", color="#2b2b2b"), |
| 116 | + panel_grid_major_x=element_blank(), |
| 117 | + panel_grid_minor=element_blank(), |
| 118 | + panel_grid_major_y=element_line(alpha=0.15, size=0.4, color="#cccccc"), |
| 119 | + panel_background=element_rect(fill="white", color="none"), |
| 120 | + plot_background=element_rect(fill="#fafafa", color="none"), |
86 | 121 | legend_position="none", |
87 | 122 | ) |
88 | 123 | ) |
|
0 commit comments