Ctrl K
ComunidadGuías…

history_2_js.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#! /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())