from base64 import b64decode
from io import BytesIO
from io import StringIO
from json import dumps, loads
from onegov.core.custom import json
from onegov.core.utils import module_path
from onegov.election_day import _
from onegov.election_day.models import Ballot
from onegov.election_day.models import Election
from onegov.election_day.models import ElectionCompound
from onegov.election_day.models import ElectionCompoundPart
from onegov.election_day.utils.election import get_candidates_data
from onegov.election_day.utils.election import get_connections_data
from onegov.election_day.utils.election import get_lists_data
from onegov.election_day.utils.election import get_lists_panachage_data
from onegov.election_day.utils.election_compound import get_list_groups_data
from onegov.election_day.utils.parties import get_parties_panachage_data
from onegov.election_day.utils.parties import get_party_results_data
from onegov.election_day.utils.parties import get_party_results_vertical_data
from onegov.election_day.utils.vote import get_ballot_data_by_district
from onegov.election_day.utils.vote import get_ballot_data_by_entity
from requests import post
from rjsmin import jsmin # type:ignore[import-untyped]
from typing import overload
from typing import Any
from typing import IO
from typing import Literal
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from onegov.core.types import JSON_ro
from onegov.core.types import JSONObject_ro
from onegov.election_day.app import ElectionDayApp
from translationstring import TranslationString
[docs]
class D3Renderer:
""" Provides access to the d3-renderer (github.com/seantis/d3-renderer).
"""
def __init__(self, app: 'ElectionDayApp'):
[docs]
self.renderer = app.configuration.get('d3_renderer', '').rstrip('/')
[docs]
self.supported_charts = {
'bar': {
'main': 'barChart',
'scripts': ('d3.chart.bar.js',),
},
'grouped': {
'main': 'groupedChart',
'scripts': ('d3.chart.grouped.js',),
},
'sankey': {
'main': 'sankeyChart',
'scripts': ('d3.sankey.js', 'd3.chart.sankey.js'),
},
'entities-map': {
'main': 'entitiesMap',
'scripts': ('topojson.js', 'd3.map.entities.js'),
},
'districts-map': {
'main': 'districtsMap',
'scripts': ('topojson.js', 'd3.map.districts.js'),
}
}
# Read and minify the javascript sources
[docs]
self.scripts: dict[str, list[str]] = {}
for chart in self.supported_charts:
self.scripts[chart] = []
for script in self.supported_charts[chart]['scripts']:
path = module_path(
'onegov.election_day', 'assets/js/{}'.format(script)
)
with open(path) as f:
self.scripts[chart].append(jsmin(f.read()))
[docs]
def translate(self, text: 'TranslationString', locale: str | None) -> str:
""" Translates the given string. """
if locale is not None:
translator = self.app.translations.get(locale)
else:
translator = None
translated = translator.gettext(text) if translator else None
return text.interpolate(translated)
# def label(self):
@overload
[docs]
def get_chart(
self,
chart: str,
fmt: Literal['pdf'],
data: 'JSON_ro',
width: int = 1000,
params: dict[str, Any] | None = None
) -> IO[bytes]: ...
@overload
def get_chart(
self,
chart: str,
fmt: Literal['svg'],
data: 'JSON_ro',
width: int = 1000,
params: dict[str, Any] | None = None
) -> IO[str]: ...
@overload
def get_chart(
self,
chart: str,
fmt: Literal['pdf', 'svg'],
data: 'JSON_ro',
width: int = 1000,
params: dict[str, Any] | None = None
) -> IO[str] | IO[bytes]: ...
def get_chart(
self,
chart: str,
fmt: Literal['pdf', 'svg'],
data: 'JSON_ro',
width: int = 1000,
params: dict[str, Any] | None = None
) -> IO[str] | IO[bytes]:
""" Returns the requested chart from the d3-render service as a
PNG/PDF/SVG.
"""
assert chart in self.supported_charts
assert fmt in ('pdf', 'svg')
params = params or {}
params.update({
'data': data,
'width': width,
'viewport_width': width # only used for PDF and PNG
})
response = post(
'{}/d3/{}'.format(self.renderer, fmt),
json={
'scripts': self.scripts[chart],
'main': self.supported_charts[chart]['main'],
'params': loads(dumps(params).replace("'", '’')) # noqa:RUF001
},
timeout=60
)
response.raise_for_status()
if fmt == 'svg':
return StringIO(response.text)
else:
return BytesIO(b64decode(response.text))
@overload
[docs]
def get_map(
self,
map: str,
fmt: Literal['pdf'],
data: 'JSON_ro',
year: int,
width: int = 1000,
params: dict[str, Any] | None = None
) -> IO[bytes]: ...
@overload
def get_map(
self,
map: str,
fmt: Literal['svg'],
data: 'JSON_ro',
year: int,
width: int = 1000,
params: dict[str, Any] | None = None
) -> IO[str]: ...
@overload
def get_map(
self,
map: str,
fmt: Literal['svg', 'pdf'],
data: 'JSON_ro',
year: int,
width: int = 1000,
params: dict[str, Any] | None = None
) -> IO[str] | IO[bytes]: ...
def get_map(
self,
map: str,
fmt: Literal['pdf', 'svg'],
data: 'JSON_ro',
year: int,
width: int = 1000,
params: dict[str, Any] | None = None
) -> IO[str] | IO[bytes]:
""" Returns the request chart from the d3-render service as a
PNG/PDF/SVG.
"""
mapdata = None
path = module_path(
'onegov.election_day',
f'static/mapdata/{year}/{self.app.principal.id}.json'
)
with open(path) as f:
mapdata = json.loads(f.read())
params = params or {}
params.update({
'mapdata': mapdata,
'canton': self.app.principal.id
})
return self.get_chart(f'{map}-map', fmt, data, width, params)
@overload
[docs]
def get_list_groups_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[False] = False,
) -> IO[bytes] | None: ...
@overload
def get_list_groups_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[False] = False,
) -> IO[str] | None: ...
@overload
def get_list_groups_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[True],
) -> tuple[IO[bytes] | None, Any | None]: ...
@overload
def get_list_groups_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[True],
) -> tuple[IO[str] | None, Any | None]: ...
def get_list_groups_chart(
self,
item: object,
fmt: Literal['svg', 'pdf'],
return_data: bool = False,
) -> IO[Any] | tuple[IO[Any] | None, Any | None] | None:
chart = None
data = None
if isinstance(item, ElectionCompound):
data = get_list_groups_data(item)
if data and data.get('results'):
chart = self.get_chart('bar', fmt, data)
return (chart, data) if return_data else chart
@overload
[docs]
def get_lists_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[False] = False,
) -> IO[bytes] | None: ...
@overload
def get_lists_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[False] = False,
) -> IO[str] | None: ...
@overload
def get_lists_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[True],
) -> tuple[IO[bytes] | None, Any | None]: ...
@overload
def get_lists_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[True],
) -> tuple[IO[str] | None, Any | None]: ...
def get_lists_chart(
self,
item: object,
fmt: Literal['svg', 'pdf'],
return_data: bool = False,
) -> IO[Any] | tuple[IO[Any] | None, Any | None] | None:
chart = None
data = None
if isinstance(item, Election):
data = get_lists_data(item)
if data and data.get('results'):
chart = self.get_chart('bar', fmt, data)
return (chart, data) if return_data else chart
@overload
[docs]
def get_candidates_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[False] = False,
) -> IO[bytes] | None: ...
@overload
def get_candidates_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[False] = False,
) -> IO[str] | None: ...
@overload
def get_candidates_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[True],
) -> tuple[IO[bytes] | None, Any | None]: ...
@overload
def get_candidates_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[True],
) -> tuple[IO[str] | None, Any | None]: ...
def get_candidates_chart(
self,
item: object,
fmt: Literal['svg', 'pdf'],
return_data: bool = False,
) -> IO[Any] | tuple[IO[Any] | None, Any | None] | None:
chart = None
data = None
if isinstance(item, Election):
data = get_candidates_data(item)
if data and data.get('results'):
chart = self.get_chart('bar', fmt, data)
return (chart, data) if return_data else chart
@overload
[docs]
def get_connections_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[False] = False,
) -> IO[bytes] | None: ...
@overload
def get_connections_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[False] = False,
) -> IO[str] | None: ...
@overload
def get_connections_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[True],
) -> tuple[IO[bytes] | None, Any | None]: ...
@overload
def get_connections_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[True],
) -> tuple[IO[str] | None, Any | None]: ...
def get_connections_chart(
self,
item: object,
fmt: Literal['svg', 'pdf'],
return_data: bool = False,
) -> IO[Any] | tuple[IO[Any] | None, Any | None] | None:
chart = None
data = None
if isinstance(item, Election):
data = get_connections_data(item)
if data and data.get('links') and data.get('nodes'):
chart = self.get_chart(
'sankey', fmt, data, params={'inverse': True}
)
return (chart, data) if return_data else chart
@overload
[docs]
def get_seat_allocation_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[False] = False,
) -> IO[bytes] | None: ...
@overload
def get_seat_allocation_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[False] = False,
) -> IO[str] | None: ...
@overload
def get_seat_allocation_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[True],
) -> tuple[IO[bytes] | None, Any | None]: ...
@overload
def get_seat_allocation_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[True],
) -> tuple[IO[str] | None, Any | None]: ...
def get_seat_allocation_chart(
self,
item: object,
fmt: Literal['svg', 'pdf'],
return_data: bool = False,
) -> IO[Any] | tuple[IO[Any] | None, Any | None] | None:
chart = None
data = None
if isinstance(item, (Election, ElectionCompound)):
data = get_party_results_vertical_data(item)
if data and data.get('results'):
chart = self.get_chart(
'grouped', fmt, data, params={'showBack': False}
)
return (chart, data) if return_data else chart
@overload
[docs]
def get_party_strengths_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[False] = False,
) -> IO[bytes] | None: ...
@overload
def get_party_strengths_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[False] = False,
) -> IO[str] | None: ...
@overload
def get_party_strengths_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[True],
) -> tuple[IO[bytes] | None, Any | None]: ...
@overload
def get_party_strengths_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[True],
) -> tuple[IO[str] | None, Any | None]: ...
def get_party_strengths_chart(
self,
item: object,
fmt: Literal['svg', 'pdf'],
return_data: bool = False,
) -> IO[Any] | tuple[IO[Any] | None, Any | None] | None:
chart = None
data = None
if isinstance(
item, (Election, ElectionCompound, ElectionCompoundPart)
):
data = get_party_results_data(
item, item.horizontal_party_strengths
)
if data and data.get('results'):
if item.horizontal_party_strengths:
chart = self.get_chart('bar', fmt, data)
else:
chart = self.get_chart(
'grouped', fmt, data, params={'showBack': False}
)
return (chart, data) if return_data else chart
@overload
[docs]
def get_lists_panachage_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[False] = False,
) -> IO[bytes] | None: ...
@overload
def get_lists_panachage_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[False] = False,
) -> IO[str] | None: ...
@overload
def get_lists_panachage_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[True],
) -> tuple[IO[bytes] | None, Any | None]: ...
@overload
def get_lists_panachage_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[True],
) -> tuple[IO[str] | None, Any | None]: ...
def get_lists_panachage_chart(
self,
item: object,
fmt: Literal['svg', 'pdf'],
return_data: bool = False,
) -> IO[Any] | tuple[IO[Any] | None, Any | None] | None:
chart = None
data = None
if isinstance(item, Election):
data = get_lists_panachage_data(item, None)
if data and data.get('links') and data.get('nodes'):
chart = self.get_chart('sankey', fmt, data)
return (chart, data) if return_data else chart
@overload
[docs]
def get_parties_panachage_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[False] = False,
) -> IO[bytes] | None: ...
@overload
def get_parties_panachage_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[False] = False,
) -> IO[str] | None: ...
@overload
def get_parties_panachage_chart(
self,
item: object,
fmt: Literal['pdf'],
return_data: Literal[True],
) -> tuple[IO[bytes] | None, Any | None]: ...
@overload
def get_parties_panachage_chart(
self,
item: object,
fmt: Literal['svg'],
return_data: Literal[True],
) -> tuple[IO[str] | None, Any | None]: ...
def get_parties_panachage_chart(
self,
item: object,
fmt: Literal['svg', 'pdf'],
return_data: bool = False,
) -> IO[Any] | tuple[IO[Any] | None, Any | None] | None:
chart = None
data = None
if isinstance(item, (Election, ElectionCompound)):
data = get_parties_panachage_data(item, None)
if data and data.get('links') and data.get('nodes'):
chart = self.get_chart('sankey', fmt, data)
return (chart, data) if return_data else chart
@overload
[docs]
def get_entities_map(
self,
item: object,
fmt: Literal['pdf'],
locale: str,
return_data: Literal[False] = False,
) -> IO[bytes] | None: ...
@overload
def get_entities_map(
self,
item: object,
fmt: Literal['svg'],
locale: str,
return_data: Literal[False] = False,
) -> IO[str] | None: ...
@overload
def get_entities_map(
self,
item: object,
fmt: Literal['pdf'],
locale: str,
return_data: Literal[True],
) -> tuple[IO[bytes] | None, Any | None]: ...
@overload
def get_entities_map(
self,
item: object,
fmt: Literal['svg'],
locale: str,
return_data: Literal[True],
) -> tuple[IO[str] | None, Any | None]: ...
def get_entities_map(
self,
item: object,
fmt: Literal['svg', 'pdf'],
locale: str,
return_data: bool = False,
) -> IO[Any] | tuple[IO[Any] | None, Any | None] | None:
chart = None
data: JSONObject_ro | None = None
if isinstance(item, Ballot):
data = get_ballot_data_by_entity(item) # type:ignore[assignment]
if data:
label_left = _('Nay')
label_right = _('Yay')
tie_breaker = (
item.type == 'tie-breaker'
or item.vote.tie_breaker_vocabulary
)
if tie_breaker:
label_left = (
_('Direct Counter Proposal') if item.vote.direct
else _('Indirect Counter Proposal')
)
label_right = _('Proposal')
params = {
'labelLeftHand': self.translate(label_left, locale),
'labelRightHand': self.translate(label_right, locale),
}
year = item.vote.date.year
chart = self.get_map(
'entities', fmt, data, year, params=params
)
return (chart, data) if return_data else chart
@overload
[docs]
def get_districts_map(
self,
item: object,
fmt: Literal['pdf'],
locale: str,
return_data: Literal[False] = False,
) -> IO[bytes] | None: ...
@overload
def get_districts_map(
self,
item: object,
fmt: Literal['svg'],
locale: str,
return_data: Literal[False] = False,
) -> IO[str] | None: ...
@overload
def get_districts_map(
self,
item: object,
fmt: Literal['pdf'],
locale: str,
return_data: Literal[True],
) -> tuple[IO[bytes] | None, Any | None]: ...
@overload
def get_districts_map(
self,
item: object,
fmt: Literal['svg'],
locale: str,
return_data: Literal[True],
) -> tuple[IO[str] | None, Any | None]: ...
def get_districts_map(
self,
item: object,
fmt: Literal['svg', 'pdf'],
locale: str,
return_data: bool = False,
) -> IO[Any] | tuple[IO[Any] | None, Any | None] | None:
chart = None
data: JSONObject_ro | None = None
if isinstance(item, Ballot):
data = get_ballot_data_by_district(item) # type:ignore[assignment]
if data:
label_left = _('Nay')
label_right = _('Yay')
tie_breaker = (
item.type == 'tie-breaker'
or item.vote.tie_breaker_vocabulary
)
if tie_breaker:
label_left = (
_('Direct Counter Proposal') if item.vote.direct
else _('Indirect Counter Proposal')
)
label_right = _('Proposal')
params = {
'labelLeftHand': self.translate(label_left, locale),
'labelRightHand': self.translate(label_right, locale),
}
year = item.vote.date.year
chart = self.get_map(
'districts', fmt, data, year, params=params
)
return (chart, data) if return_data else chart