src.plot
1import random 2from pathlib import Path 3 4import numpy as np 5import pandas as pd 6from bokeh.models import LegendItem, GlyphRenderer, Axis, DatetimeTickFormatter, DatetimeTicker, LinearAxis, Grid, \ 7 MultiLine 8 9from src.boxes import build_boxes 10from src.draw import DrawOptions, draw_parlament 11from src.util import font_path, html_path 12 13s = 43 14random.seed(s) 15np.random.seed(s) 16 17PARTY_COLOR = { 18 "F": "blue", 19 "G": "limegreen", 20 "L": "gold", 21 "S": "red", 22 "V": "black", 23 "B": "darkorange", 24 "A": "purple", 25 "T": "orange", 26 "N": "magenta", 27 "PJ": "grey" 28} 29"""Color Mapping of fractions""" 30 31PARTY_NAME = { 32 "F": "FPÖ", 33 "G": "Grüne", 34 "L": "Liberales Forum", 35 "S": "SPÖ", 36 "V": "ÖVP", 37 "B": "BZÖ", 38 "A": "ohne Klub", 39 "T": "Team Stronach", 40 "N": "NEOS", 41 "PJ": "PILZ/JETZT" 42} 43"""Full name of fractions""" 44 45GOVERNMENTS = { 46 "XX": "SPÖ/ÖVP", 47 "XXI": "ÖVP/FPÖ", 48 "XXII": "ÖVP/FPÖ", 49 "XXIII": "ÖVP/FPÖ + SPÖ/ÖVP", 50 "XXIV": "SPÖ/ÖVP", 51 "XXV": "SPÖ/ÖVP", 52 "XXVI": "SPÖ/ÖVP + ÖVP/FPÖ", 53 "XXVII": "ÖVP/Grüne", 54} 55"""Governments during legislative periods of parliament""" 56 57 58def pt_to_px(ppi, pt: float) -> float: 59 """Converts pt to pixel""" 60 # 1 pt = 1/72 inches 61 inches = pt / 72 62 return inches * ppi 63 64 65def px_to_pt(ppi, px: int) -> float: 66 """Convert pixel to pt""" 67 inches = px / ppi 68 return inches * 72 69 70 71assert pt_to_px(100, px_to_pt(100, 10)) == 10 72 73 74def plot_bokeh(options: DrawOptions, legislative_periods: list[str], fulltext: bool = False) -> tuple[str, str]: 75 """Plots WordStream visualisation in bokeh and generates standalone html document 76 77 Keyword arguments: 78 options -- DrawOptions for bokeh plot 79 legislative_periods -- roman numerals of periods which should be plotted 80 fulltext -- true: fulltext of motions is used, false: eurovoc keywords are used 81 """ 82 83 from bokeh.io import curdoc, show 84 from bokeh.models import ColumnDataSource, Plot, Text, Range1d, HoverTool, Circle, Legend, FixedTicker 85 from bokeh.core.properties import value 86 87 placements, data = draw_parlament(options, legislative_periods=legislative_periods, fulltext=fulltext) 88 topics = list(placements.keys()) 89 90 ppi = 72 91 plot = Plot( 92 title=None, 93 # width=options.width*ppi, height=options.height*ppi, 94 frame_width=options.width * ppi, frame_height=options.height * ppi, 95 # min_width=options.width * ppi, min_height=options.height * ppi, 96 min_border=0, toolbar_location=None, 97 background_fill_color=None, border_fill_color=None, background_fill_alpha=0.0 98 ) 99 plot.output_backend = "svg" 100 101 hover = HoverTool() 102 hover.tooltips = """ 103 <div> 104 <div> 105 <span style="font-size: 17px; font-weight: bold;">@text</span><br> 106 <span style="font-size: 15px; font-weight: bold; color: @col">@topic</span> 107 </div> 108 </div> 109 """ 110 plot.add_tools(hover) 111 112 # creade boxes and add x-axis 113 boxes = build_boxes(data, options.width, options.height) 114 x_index = boxes[list(boxes.keys())[0]].index.values.tolist() 115 label_dict = {} 116 for i, s in zip(x_index, data.df["Datum"]): 117 label_dict[i] = str(s.year) 118 119 xaxis = LinearAxis(ticker=FixedTicker(ticks=x_index)) 120 plot.add_layout(xaxis, 'below') 121 plot.xaxis[0].major_label_overrides = label_dict 122 plot.add_layout(Grid(dimension=0, ticker=xaxis.ticker)) 123 124 # Find x-axis positions of start of legislative period 125 # xpos = [list(filter(lambda x: label_dict[x] == str(timestamp.year), label_dict))[0] for period, timestamp in 126 # data.segment_start.items()] 127 # generate datasource for lines for legislative periods 128 129 x_pos_list = list(label_dict.keys()) 130 dates_list = list(label_dict.values()) 131 132 period_xs = [] 133 period_ys = [] 134 period_label = [] 135 for period, timestamp in data.segment_start.items(): 136 startidx = dates_list.index(str(timestamp.year)) 137 if startidx == 0: 138 x_pos = x_pos_list[startidx] 139 elif len(dates_list) == startidx + 1: 140 # legislative period started in current year. (use the with of last year to calculate position 141 year_width = options.width - x_pos_list[startidx] 142 day_of_year = int(timestamp.strftime("%j")) 143 x_pos = x_pos_list[startidx] + year_width * day_of_year / 365 144 else: 145 year_width = x_pos_list[startidx + 1] - x_pos_list[startidx] 146 day_of_year = int(timestamp.strftime("%j")) 147 x_pos = x_pos_list[startidx] + year_width * day_of_year / 365 148 period_xs.append([x_pos, x_pos]) 149 period_ys.append([-options.height / 2, options.height / 2]) 150 period_label.append(period) 151 152 segments = ColumnDataSource(dict( 153 xs=period_xs, 154 ys=period_ys, 155 text=period_label, 156 topic=[f"Gesetzgebungsperiode {p} ({GOVERNMENTS[p]})" for p in period_label] 157 )) 158 segments_text = ColumnDataSource(dict( 159 x=[xs[0] for xs in period_xs], 160 y=[ys[1] for ys in period_ys], 161 text=period_label, 162 topic=[f"Gesetzgebungsperiode {p} ({GOVERNMENTS[p]})" for p in period_label] 163 )) 164 165 # segments = ColumnDataSource(dict( 166 # xs=[[x, x] for x in xpos], 167 # ys=[[-options.height / 2, options.height / 2] for period in data.segment_start.keys()])) 168 169 period_segments = MultiLine(xs="xs", ys="ys", line_color="black", line_width=1, line_dash="dashed") 170 plot.add_glyph(segments, period_segments) 171 period_text = Text(x='x', y='y', text='text', text_font_style='bold') 172 plot.add_glyph(segments_text, period_text) 173 174 topic_glyphs = dict() 175 for topic in topics: 176 col = PARTY_COLOR[topic] 177 df = pd.DataFrame.from_records(placements[topic]) 178 if df.size == 0: 179 # No topics of club could be placed 180 continue 181 df["font_size"] = df["font_size"].apply(lambda v: f"{pt_to_px(ppi, v)}px") 182 df["topic"] = PARTY_NAME[topic] 183 df["col"] = col 184 185 glyph = Text(x="x", y="y", text="text", text_color=col, text_font_size="fs", text_baseline="top", 186 text_align="left") 187 glyph.text_font = value("Rubik") 188 ds = ColumnDataSource( 189 dict(x=df.x, y=df.y, text=df.text, fs=df.font_size, topic=df.topic, col=df.col)) 190 plot.add_glyph(ds, glyph) 191 192 # circles need to actually be drawn so place them outside of plot area as a hack 193 ds_legend = ColumnDataSource(dict(col=df.col, x=df.x, y=df.y + 100)) 194 points = Circle(x="x", y="y", fill_color="col") 195 legend_renderer = plot.add_glyph(ds_legend, points) 196 topic_glyphs[PARTY_NAME[topic]] = legend_renderer 197 198 legend = Legend( 199 items=[(p, [t]) for p, t in topic_glyphs.items()], 200 location="center", orientation="vertical", 201 border_line_color="black", 202 title='Party', 203 background_fill_alpha=0.0 204 ) 205 plot.add_layout(legend, "left") 206 207 plot.y_range = Range1d(options.height / 2, -options.height / 2) 208 plot.x_range = Range1d(0, max(x_index) + x_index[1]) 209 210 curdoc().add_root(plot) 211 212 from bokeh.embed import components 213 plot_script, div = components(plot, wrap_script=True) 214 return plot_script, div 215 216 217if __name__ == '__main__': 218 options = DrawOptions(width=11, height=6, min_font_size=10, max_font_size=25) 219 # legislative_periods = ["XX", "XXI", "XXII","XXIII", "XXIV","XXV","XXVI", "XXVII"] 220 plots = { 221 "plot_1": plot_bokeh(options, ["XX", "XXI", "XXII"], fulltext=False), 222 "plot_2": plot_bokeh(options, ["XX", "XXI", "XXII"], fulltext=True), 223 "plot_3": plot_bokeh(options, ["XXIII", "XXIV", "XXV"], fulltext=False), 224 "plot_4": plot_bokeh(options, ["XXVI", "XXVII"], fulltext=False), 225 "plot_5": plot_bokeh(DrawOptions(width=32, height=10, min_font_size=10, max_font_size=25), 226 ["XX", "XXI", "XXII", "XXIII", "XXIV", "XXV", "XXVI", "XXVII"], 227 fulltext=False), 228 } 229 230 import jinja2 231 232 with open(font_path + "/template_custom.html", "r") as f: 233 template = jinja2.Template(f.read()) 234 235 html = template.render(**plots) 236 with open(html_path + "/index.html", "w") as f: 237 f.write(html)
PARTY_COLOR =
{'F': 'blue', 'G': 'limegreen', 'L': 'gold', 'S': 'red', 'V': 'black', 'B': 'darkorange', 'A': 'purple', 'T': 'orange', 'N': 'magenta', 'PJ': 'grey'}
Color Mapping of fractions
PARTY_NAME =
{'F': 'FPÖ', 'G': 'Grüne', 'L': 'Liberales Forum', 'S': 'SPÖ', 'V': 'ÖVP', 'B': 'BZÖ', 'A': 'ohne Klub', 'T': 'Team Stronach', 'N': 'NEOS', 'PJ': 'PILZ/JETZT'}
Full name of fractions
GOVERNMENTS =
{'XX': 'SPÖ/ÖVP', 'XXI': 'ÖVP/FPÖ', 'XXII': 'ÖVP/FPÖ', 'XXIII': 'ÖVP/FPÖ + SPÖ/ÖVP', 'XXIV': 'SPÖ/ÖVP', 'XXV': 'SPÖ/ÖVP', 'XXVI': 'SPÖ/ÖVP + ÖVP/FPÖ', 'XXVII': 'ÖVP/Grüne'}
Governments during legislative periods of parliament
def
pt_to_px(ppi, pt: float) -> float:
59def pt_to_px(ppi, pt: float) -> float: 60 """Converts pt to pixel""" 61 # 1 pt = 1/72 inches 62 inches = pt / 72 63 return inches * ppi
Converts pt to pixel
def
px_to_pt(ppi, px: int) -> float:
66def px_to_pt(ppi, px: int) -> float: 67 """Convert pixel to pt""" 68 inches = px / ppi 69 return inches * 72
Convert pixel to pt
def
plot_bokeh( options: src.draw.DrawOptions, legislative_periods: list[str], fulltext: bool = False) -> tuple[str, str]:
75def plot_bokeh(options: DrawOptions, legislative_periods: list[str], fulltext: bool = False) -> tuple[str, str]: 76 """Plots WordStream visualisation in bokeh and generates standalone html document 77 78 Keyword arguments: 79 options -- DrawOptions for bokeh plot 80 legislative_periods -- roman numerals of periods which should be plotted 81 fulltext -- true: fulltext of motions is used, false: eurovoc keywords are used 82 """ 83 84 from bokeh.io import curdoc, show 85 from bokeh.models import ColumnDataSource, Plot, Text, Range1d, HoverTool, Circle, Legend, FixedTicker 86 from bokeh.core.properties import value 87 88 placements, data = draw_parlament(options, legislative_periods=legislative_periods, fulltext=fulltext) 89 topics = list(placements.keys()) 90 91 ppi = 72 92 plot = Plot( 93 title=None, 94 # width=options.width*ppi, height=options.height*ppi, 95 frame_width=options.width * ppi, frame_height=options.height * ppi, 96 # min_width=options.width * ppi, min_height=options.height * ppi, 97 min_border=0, toolbar_location=None, 98 background_fill_color=None, border_fill_color=None, background_fill_alpha=0.0 99 ) 100 plot.output_backend = "svg" 101 102 hover = HoverTool() 103 hover.tooltips = """ 104 <div> 105 <div> 106 <span style="font-size: 17px; font-weight: bold;">@text</span><br> 107 <span style="font-size: 15px; font-weight: bold; color: @col">@topic</span> 108 </div> 109 </div> 110 """ 111 plot.add_tools(hover) 112 113 # creade boxes and add x-axis 114 boxes = build_boxes(data, options.width, options.height) 115 x_index = boxes[list(boxes.keys())[0]].index.values.tolist() 116 label_dict = {} 117 for i, s in zip(x_index, data.df["Datum"]): 118 label_dict[i] = str(s.year) 119 120 xaxis = LinearAxis(ticker=FixedTicker(ticks=x_index)) 121 plot.add_layout(xaxis, 'below') 122 plot.xaxis[0].major_label_overrides = label_dict 123 plot.add_layout(Grid(dimension=0, ticker=xaxis.ticker)) 124 125 # Find x-axis positions of start of legislative period 126 # xpos = [list(filter(lambda x: label_dict[x] == str(timestamp.year), label_dict))[0] for period, timestamp in 127 # data.segment_start.items()] 128 # generate datasource for lines for legislative periods 129 130 x_pos_list = list(label_dict.keys()) 131 dates_list = list(label_dict.values()) 132 133 period_xs = [] 134 period_ys = [] 135 period_label = [] 136 for period, timestamp in data.segment_start.items(): 137 startidx = dates_list.index(str(timestamp.year)) 138 if startidx == 0: 139 x_pos = x_pos_list[startidx] 140 elif len(dates_list) == startidx + 1: 141 # legislative period started in current year. (use the with of last year to calculate position 142 year_width = options.width - x_pos_list[startidx] 143 day_of_year = int(timestamp.strftime("%j")) 144 x_pos = x_pos_list[startidx] + year_width * day_of_year / 365 145 else: 146 year_width = x_pos_list[startidx + 1] - x_pos_list[startidx] 147 day_of_year = int(timestamp.strftime("%j")) 148 x_pos = x_pos_list[startidx] + year_width * day_of_year / 365 149 period_xs.append([x_pos, x_pos]) 150 period_ys.append([-options.height / 2, options.height / 2]) 151 period_label.append(period) 152 153 segments = ColumnDataSource(dict( 154 xs=period_xs, 155 ys=period_ys, 156 text=period_label, 157 topic=[f"Gesetzgebungsperiode {p} ({GOVERNMENTS[p]})" for p in period_label] 158 )) 159 segments_text = ColumnDataSource(dict( 160 x=[xs[0] for xs in period_xs], 161 y=[ys[1] for ys in period_ys], 162 text=period_label, 163 topic=[f"Gesetzgebungsperiode {p} ({GOVERNMENTS[p]})" for p in period_label] 164 )) 165 166 # segments = ColumnDataSource(dict( 167 # xs=[[x, x] for x in xpos], 168 # ys=[[-options.height / 2, options.height / 2] for period in data.segment_start.keys()])) 169 170 period_segments = MultiLine(xs="xs", ys="ys", line_color="black", line_width=1, line_dash="dashed") 171 plot.add_glyph(segments, period_segments) 172 period_text = Text(x='x', y='y', text='text', text_font_style='bold') 173 plot.add_glyph(segments_text, period_text) 174 175 topic_glyphs = dict() 176 for topic in topics: 177 col = PARTY_COLOR[topic] 178 df = pd.DataFrame.from_records(placements[topic]) 179 if df.size == 0: 180 # No topics of club could be placed 181 continue 182 df["font_size"] = df["font_size"].apply(lambda v: f"{pt_to_px(ppi, v)}px") 183 df["topic"] = PARTY_NAME[topic] 184 df["col"] = col 185 186 glyph = Text(x="x", y="y", text="text", text_color=col, text_font_size="fs", text_baseline="top", 187 text_align="left") 188 glyph.text_font = value("Rubik") 189 ds = ColumnDataSource( 190 dict(x=df.x, y=df.y, text=df.text, fs=df.font_size, topic=df.topic, col=df.col)) 191 plot.add_glyph(ds, glyph) 192 193 # circles need to actually be drawn so place them outside of plot area as a hack 194 ds_legend = ColumnDataSource(dict(col=df.col, x=df.x, y=df.y + 100)) 195 points = Circle(x="x", y="y", fill_color="col") 196 legend_renderer = plot.add_glyph(ds_legend, points) 197 topic_glyphs[PARTY_NAME[topic]] = legend_renderer 198 199 legend = Legend( 200 items=[(p, [t]) for p, t in topic_glyphs.items()], 201 location="center", orientation="vertical", 202 border_line_color="black", 203 title='Party', 204 background_fill_alpha=0.0 205 ) 206 plot.add_layout(legend, "left") 207 208 plot.y_range = Range1d(options.height / 2, -options.height / 2) 209 plot.x_range = Range1d(0, max(x_index) + x_index[1]) 210 211 curdoc().add_root(plot) 212 213 from bokeh.embed import components 214 plot_script, div = components(plot, wrap_script=True) 215 return plot_script, div
Plots WordStream visualisation in bokeh and generates standalone html document
Keyword arguments: options -- DrawOptions for bokeh plot legislative_periods -- roman numerals of periods which should be plotted fulltext -- true: fulltext of motions is used, false: eurovoc keywords are used