from __future__ import annotations
from base64 import b64decode
from io import BytesIO
from io import StringIO
from json import dumps, loads
from niquests import post
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 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()
assert response.text is not None
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