# F1 逐站表现与赛程数据维护

`index.html` 为引导页，顶部显示下一场大奖赛；`constructors.html` 是 1958—2025 车队历史流图。`drivers.html` 展示 1950—2026 年车手正赛 / 排位赛名次与冠军积分，`teams.html` 展示 1958—2026 年车队逐站积分和冠军积分，二者默认打开当前赛季。`calendar.html` 为本季时间表。`race-atlas.html` 保留综合看板，兼容原入口。图表和数据打包在 HTML 中，头像使用公开图片网址，离线时显示姓名缩写。

## 更新数据

在项目目录运行以下命令。历史数据复用缓存，第一条命令刷新当前赛季，第二条将新数据写入 HTML。

```sh
python3 scripts/update_race_data.py --refresh-current
python3 scripts/build_site.py
```

`--refresh-all` 刷新 2011 年以后的 Jolpica 档案。1950—2010 年使用带 SHA256 校验的 F1DB 固定版本，保留逐站冠军积分榜；更新其版本需调整 `scripts/extend_history.py` 的 `VERSION` 和 `SHA256`，再重新核对。只重建缓存中的数据时不传刷新参数。Jolpica 限流为每秒 4 次、每小时 500 次；脚本串行请求，遇到限流会退避重试，使用分页避免丢失结果。

换到 2027 年时使用明确的年份，再重新构建：

```sh
python3 scripts/update_race_data.py --start 1950 --end 2027 --current-season 2027 --refresh-current
python3 scripts/build_site.py
```

当前年份显式传入，避免维护者的设备日期影响展示范围。首次获取数据需网络；已有快照可以离线重新构建。积分核对失败时会保留诊断数据，但构建脚本会拒绝发布有差异的数据。不要删除核对结果或强行跳过断言；先确认源数据同步状态及处罚记录。冲刺赛后的积分榜可能稍晚于赛果更新，必要时等接口同步后重试。

## 文件与来源

| 文件 | 用途 |
| --- | --- |
| `data/race-atlas.json` | 标准化后的完整绘图数据，版本为 `schemaVersion: 1` |
| `data/race-raw/` | 按年份、端点和分页保存的原始响应，附来源网址和抓取时间 |
| `data/f1db/` | 历史原始发行包、版本、SHA256 和来源记录；CC BY 4.0 |
| `data/race-overrides.json` | 更名分段、扣分事件、时间和头像的人工维护入口 |
| `src/race-atlas.html` | 页面样式与交互源码 |
| `scripts/update_race_data.py` | 分页获取数据，生成图表序列，核对赛季总分 |
| `scripts/build_race_atlas.py` | 打包 D3 和数据，生成独立 HTML |
| `scripts/extend_history.py` | 历史适配器；冠军积分直接取逐站榜单，并与官方车队表核对 |
| `scripts/qualifying_data.py` | 从校验过的 F1DB 发行包和 Jolpica 缓存附加排位赛最终分类 |
| `src/schedule.js` | 本地时间判断、预估时长、跨日分组和自动切换；首页与赛程共用 |
| `scripts/build_site.py` | 重建首页与所有子页 |
| `scripts/check_race_atlas.cjs` | 浏览器验证、手机布局与页面截图 |
| `scripts/check_refinements.cjs` | 360 / 390 / 430px 信息密度、全屏横屏、排位、时间切换与占比矩形验证 |

1950—2010 年数据来自 [F1DB v2026.16.0](https://github.com/f1db/f1db/releases/tag/v2026.16.0)，由 F1DB / Marcel Overdijk 和贡献者提供，许可证 CC BY 4.0；本项目转换结构、合并底盘版本并应用显式修正。2011 年以后的名次、积分、赛程和车手总榜来自 [Jolpica F1](https://github.com/jolpica/jolpica-f1/blob/main/docs/README.md)。分页上限 100 指结果行数，不是大奖赛数量；脚本按分站编号合并跨页结果。1958—2025 年车队最终总分与项目已有的 [F1 官方积分档案](https://www.formula1.com/en/results/2025/team) 核对，当前赛季使用 Jolpica 总榜。赛程可与 [F1 官方赛历](https://www.formula1.com/en/racing/2026) 对照。公开头像网址来自 [OpenF1](https://openf1.org/)，不保证是相应年份的历史造型。

2026 年 10 月 8 日的数据快照包含 77 个可查询赛季、1,165 场已归档正赛及对应排位赛分类、862 位车手档案（含只在排位档案中出现的车手）、28 位车手的公开头像，以及本季 7 场待进行赛事。累计积分最终值核对覆盖 3,266 个车手赛季记录和 912 个车队赛季记录；历史车队另与官方表核对 744 条，差异数为 0。1960 年 Porsche 的来源车队 1 分与官方表不符，已通过 `historicalConstructorExclusions` 显式归零，保留车手得分和修正来源。

排位分类初始来自同一 [F1DB v2026.16.0](https://github.com/f1db/f1db/releases/tag/v2026.16.0) 校验发行包，CC BY 4.0；随后由更新脚本取得的 Jolpica `qualifying` 分页覆盖相应分站。它是最终排位分类，不是受发车处罚影响后的正赛发车位。当前发行包截止 2026 年第 16 站；新排位结果无需等待 F1DB 更新，可由 `--refresh-current` 获取。

## 图表计算口径

车手名次为正赛最终分类名次，不是车手冠军积分榜排名。退赛车手的有序分类名次仍绘制，并显示空心点。取消资格和未发车不作为正常名次绘制；未参赛、赛果未更新及未来场次使用 `null`。所有状态均保留，不能用 0 替代缺失名次。

正赛 / 排位名次曲线只在实际有分类名次的点上绘制；中间缺失且该站已有档案时，以虚线连接前后有效点，表示缺失区间，不产生补算的名次。未来场次不连虚线。排位分类取消资格优先于同一来源中保留的无成绩记录；例如 2023 年英国站 Bottas，原始两条记录保留，但曲线状态为 DSQ。缺失头像、排位记录和参赛场次均允许为空。

累计积分使用当年的实际计分制度，不做跨年代换算。历史流图同样只显示官方原始积分，分段尺度仅影响色带绘制。1950—2010 年直接读取档案中每站后的冠军积分，保留最佳成绩 / 舍弃分限制；例如 1988 年 Prost 实得 105 分，但冠军计入 87 分。2011 年以后按正赛及冲刺得分累加并应用扣分，含最快圈、半分和双倍积分。全部是现存档案值，不保证复原当晚发布的未经追溯处罚榜单。

车队每站图按当年构造商资格计实得分：1958—1978 年每种底盘 / 发动机组合只计最佳赛车，1961 年胜者车队得 8 分、车手得 9 分，印第安纳波利斯不计构造商分；引擎版本在界面按底盘合并。1979 年起相加符合资格的车手得分。累计值与实得分之间的舍弃分、计分资格或处罚差额单列调整。1950—1957 年保留车手所属车队信息，但所有车队积分为 `null`，不生成虚构锦标赛。

2018 年旧 Force India 与 Racing Point Force India 是两个参赛主体，旧主体在第 13 站清零；车手积分保留。[F1 官方说明](https://www.formula1.com/en/results/2018/team/Force-India-Mercedes)。2020 年 Racing Point 扣 15 分，在第 5 站周末加入调整，不扣车手分。[F1 处罚报道](https://www.formula1.com/en/latest/article/breaking-racing-point-deducted-15-points-and-fined-heavily-as-renault.7j9tsLSAm7rXnWHxH1Uy9c)。

早期共用赛车和同站驾驶多辆赛车的赛果明细全部保留；名次曲线每位车手每站仅取最佳分类，单站车手实得分相加，冠军累计取榜单。`sharedCar` 和 `carNumber` 保留原记录。退赛无正式数值名次时显示档案中的结果排序，并用空心点明确标记。

同一车手中途换队仍保留一条年度曲线，`teamIds` 保留效力顺序。颜色使用本赛季最后效力的车队，同队车手用不同线型。车队数据按赛季独立，不沿用历史流图的组织承接合并。

## 数据结构

顶层包含 `schemaVersion`、`generatedAt`、`currentSeason`、`startSeason`、`endSeason`、`drivers`、`seasons`、`sources` 和 `audit`。

`drivers[driverId]` 保存姓名、缩写、车号、国籍及可选的 `headshotUrl`、`portraitSource`、`portraitYear`。缺失头像可以不填写网址。

`seasons[year]` 包含 `races`、`drivers`、`teams`、`completedRounds`、`latestRound`。历史赛季另有 `cumulativeMode: "championshipStandings"`、`hasConstructorChampionship`，以及逐站 `driverStandings` / `teamStandings`。`races` 按实际分站编号递增。车手和车队的数组均与 `races` 一一对应，不直接以 `round - 1` 索引。

| 字段 | 含义 |
| --- | --- |
| `races[].id` | 稳定编号，如 `2026:17` |
| `name` 和 `nameZh` | 官方英文名称及中文显示名称 |
| `date` 和 `startUtc` | 正赛日期及带 UTC 时区的开始时间 |
| `sessions[]` | 练习、排位、冲刺和正赛的名称、类型及 `startUtc` |
| `sessions[].durationMinutes` | 可选预估时长，单位分钟；缺省使用该场次类型的默认时长 |
| `hasResults` | 是否有正赛赛果，不根据日期自动设为真 |
| `hasSprintResults` 和 `hasPoints` | 是否有冲刺结果或可计入的积分 |
| `results[]` 和 `sprintResults[]` | 逐车手的名次、车队、积分、发车位及完成状态 |
| `qualifyingResults[]` 和 `hasQualifyingResults` | 排位赛分类原始明细及是否存在分类档案 |
| `adjustments[]` | 该站生效的处罚、修正及来源 |
| `drivers[].positions` 和 `statuses` | 正赛名次及状态序列，空缺为 `null` |
| `drivers[].qualifyingPositions` 和 `qualifyingStatuses` | 排位分类名次及状态；与分站一一对应，旧 v1 数据可不含这两项 |
| `points` | 每站正赛加冲刺的实得积分，未有赛果时为 `null` |
| `cumulative` | 按当年规则计入冠军的积分；历史读取逐站榜单，现代为实得分加调整；未来站点为 `null` |
| `teams[].adjustments` | 每站车队调整分，通常为 0，扣分为负数 |
| `totalPoints` | 最新可用累计积分 |

赛程时间始终存 UTC，例如 `2026-10-11T12:00:00Z`；不要把北京时间写入 UTC 字段。页面用 `Intl.DateTimeFormat` 转换时区，处理夏令时与跨日。未公布时间写 `null`，未来赛果和名次也写 `null`。已完成冲刺而正赛尚未开始时，累计总分可先计入冲刺积分，正赛名次仍留空。

## 人工维护入口

更改 `data/race-overrides.json` 后，重新运行更新与构建脚本。头像在 `driverProfiles` 按车手 ID 维护；赛程以 `年份:分站编号` 为键。只填需要覆盖的字段，不必复制全部原始数据。

```json
{
  "races": {
    "2026:17": {
      "nameZh": "新加坡",
      "sessions": {
        "qualifying": "2026-10-10T13:00:00Z",
        "race": "2026-10-11T12:00:00Z"
      },
      "sessionDurations": {
        "race": 240
      }
    }
  },
  "driverProfiles": {
    "norris": {
      "headshotUrl": "https://公开图片地址",
      "portraitSource": "https://图片来源页面"
    }
  }
}
```

上面的头像网址是占位说明，维护时替换为已核实可访问的图片地址。`sessions` 可使用 `fp1`、`fp2`、`fp3`、`sprintQualifying`、`sprint`、`qualifying`、`race`；将时间设为 `null` 会显示“待公布”。`calendar` 可覆盖接口原有的正赛 `date`、`time`、`raceName` 和场地字段，`sprintWeekend: true` 可以预留冲刺周末的未知时间。

`sessionDurations` 使用相同的场次键覆盖预估时长，写入最终数据的 `durationMinutes`；范围为大于 0、至多 1440 分钟。默认练习 60、冲刺排位 45、冲刺 60、排位 90、正赛 240 分钟。预估“进行中*”不代表实时赛事状态；红旗、延迟开赛等必须由维护者更新时间。页面每 15 秒按设备时钟检查，首场练习前 24 小时起突出下一场次，预计正赛结束后自动进入下一站；不会把时间经过误写为已有赛果。未知开始时间保留“待公布”，不猜测。

扣分使用顶层 `adjustments`：指定 `season`、`round`、`entity`、`id`、`delta`、`reason` 和 `source`。不得通过修改车手名次或减去车手积分伪装车队处罚。`constructorIdentityRules` 按赛季与分站范围区分参赛主体。

## 页面导入接口

页面底部的“导入更新数据”接受完整的 `race-atlas.json`（最多 50 MiB），不是只含某站的局部补丁。导入会校验版本、时间、编号、预估时长和数组长度；失败时保留原数据。导入仅影响当前页面，刷新后仍以 HTML 内嵌快照为准。

也可从浏览器控制台或未来的数据适配器调用：

```js
const response = await fetch('data/race-atlas.json');
if (!response.ok) throw new Error('读取数据失败');
const payload = await response.json();
window.RaceAtlas.loadData(payload);
```

调用前无需清除页面。若要永久发布新快照，使用构建脚本；页面不静默后台请求赛事数据，也不自动建立定时任务。未来赛程导出为 `.ics`，只含已公布的未来开始时间，不虚构比赛结束时刻。
