history_2_js.py
Rama master · réplica de lectura del repositorio original.
#! /usr/bin/env python
# coding=utf8
'''
Convert apigh/history.json to star-history.js, the star series the interactive
chart on the page reads.
The chart needs one point list per project, not the full history: history.json
keeps every recorded change of eight fields for 64 projects, which is far too
much to ship to a browser. This script keeps only `stars`, drops the samples
plot-stars.py also drops, and downsamples each series to the points that are
visible at chart resolution.
'''
import datetime
import json
import yaml
from atomic_io import atomic_write_text
from history_store import load_history
# Carried over from plot-stars.py so the SVG and the interactive chart show the
# same seven series with the same line styles.
DEFAULT_SERIES = {
'isso': 'solid',
'commento': 'solid',
'Waline': 'dotted',
'staticman': 'dashed',
'Artalk': 'solid',
'remark': 'solid',
'valine': 'dashdot',
}
# plot-stars.py skips this window for isso: the API reported a star count that
# was never real, and the spike dominates the plot.
BAD_WINDOW = {
'isso': (datetime.date(2024, 3, 6), datetime.date(2024, 3, 12)),
}
MIN_DAYS = 30
MIN_RELATIVE_CHANGE = 0.03
def usable_points(history, project, strict=False):
'''Star events for one project, without the samples the plot rejects.
A few projects carry non-integer star values recorded before the history
was validated; they are skipped. For the default series a bad value is an
error instead, matching what plot-stars.py does for the same seven.
'''
events = history['projects'].get(project, {}).get('stars', [])
window = BAD_WINDOW.get(project)
points = []
for event_date, value in events:
if isinstance(value, bool) or not isinstance(value, int):
if strict:
raise RuntimeError(
'Invalid star history value for {} on {}: {!r}'.format(
project,
event_date,
value,
)
)
continue
if value <= 0:
continue
if window:
parsed = datetime.date.fromisoformat(event_date)
if window[0] <= parsed <= window[1]:
continue
points.append([event_date, value])
return points
def downsample(points, last_date):
'''Keep the points that change the drawn line, plus the endpoints.
A point survives when it is at least MIN_DAYS away from the last kept one
or moves the value by at least MIN_RELATIVE_CHANGE. The last recorded value
is carried forward to last_date so every series reaches the right edge.
'''
if not points:
return []
kept = [points[0]]
for event_date, value in points[1:-1]:
kept_date, kept_value = kept[-1]
days = (
datetime.date.fromisoformat(event_date)
- datetime.date.fromisoformat(kept_date)
).days
change = abs(value - kept_value) / max(kept_value, 1)
if days >= MIN_DAYS or change >= MIN_RELATIVE_CHANGE:
kept.append([event_date, value])
if len(points) > 1 and kept[-1] != points[-1]:
kept.append(points[-1])
if kept[-1][0] != last_date:
kept.append([last_date, kept[-1][1]])
return kept
def generate_star_history_js(history, keys):
last_date = max(history['dates']) if history['dates'] else None
if last_date is None:
raise RuntimeError('history contains no dates')
unknown = [name for name in DEFAULT_SERIES if name not in keys]
if unknown:
raise RuntimeError(
'default chart series missing from data.yaml: {}'.format(', '.join(unknown))
)
series = {}
first_date = None
for index, name in enumerate(keys):
points = downsample(
usable_points(history, name, strict=name in DEFAULT_SERIES),
last_date,
)
if len(points) < 2:
continue
entry = {'p': points}
dash = DEFAULT_SERIES.get(name)
if dash:
entry['d'] = dash
series[index] = entry
if first_date is None or points[0][0] < first_date:
first_date = points[0][0]
missing = [name for name in DEFAULT_SERIES if keys.index(name) not in series]
if missing:
raise RuntimeError(
'no usable star history for default series: {}'.format(', '.join(missing))
)
default_rows = [keys.index(name) for name in DEFAULT_SERIES]
return '\n'.join([
'var osc_history = {};'.format(
json.dumps(
{str(index): entry for index, entry in sorted(series.items())},
ensure_ascii=False,
separators=(',', ':'),
)
),
'var osc_history_range = {};'.format(
json.dumps({'first': first_date, 'last': last_date}, ensure_ascii=False)
),
'var osc_history_default = {};'.format(json.dumps(default_rows)),
]) + '\n'
def main():
with open('data.yaml', 'r', encoding='utf-8') as source:
data = yaml.load(source, Loader=yaml.SafeLoader)
atomic_write_text(
'star-history.js',
generate_star_history_js(load_history(), list(data)),
)
print('star-history.js has been updated')
return 0
if __name__ == '__main__':
raise SystemExit(main())