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