今週の感染症リスク
設定難易度

インフルエンザや新型コロナ、RSウイルスなど、現在どの感染症が増えているのかを確認できるウィジェットです。
郵便番号から都道府県を自動判定し、JIHS(国立健康危機管理研究機構)が公開している「感染症発生動向調査」の速報データをもとに、現在の報告状況と前週からの増減を表示します。
その地域で注目度の高い感染症を自動的に選ぶため、季節や流行状況によって表示される感染症も変化します。

小・中・大の3サイズに対応しています。

ウィジェットの表示内容

郵便番号から地域を特定し、週報の報告値を取得します。

サイズごとの表示

  • 小:注目する感染症を1件表示します。
  • 中:最大3件、大:最大7件を表示します。
  • 報告値と前週からの増減をもとに、地域の傾向を確認できます。

設定手順

上から順に進めてください。コードを保存したら、ホーム画面に追加して郵便番号を入力します。

  1. Scriptableを用意する

    App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。

  2. コードをコピーして保存する

    下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、今週の感染症のリスクなどの名前で保存してください。

    保存したらこのページに戻り、次の手順へ進みます。

    // Variables used by Scriptable.
    // These must be at the very top of the file. Do not edit.
    // icon-color: red; icon-glyph: virus;
    // Version: 1.0.0
    
    // =====================================================
    // 今週の感染症リスク - Scriptable Widget
    //
    // Parameter:
    //   郵便番号
    //
    // 例:
    //   6712500
    //   671-2500
    //
    // 小:注目感染症 TOP1
    // 中:注目感染症 TOP3
    // 大:注目感染症 TOP7
    //
    // データ:
    //   国立健康危機管理研究機構(JIHS)
    //   感染症発生動向調査週報 IDWR 速報データ
    //
    // 郵便番号:
    //   zipcloud 郵便番号検索API
    // =====================================================
    
    
    // =====================================================
    // 設定
    // =====================================================
    
    const CONFIG = {
    
      JIHS_BASE:
        "https://id-info.jihs.go.jp/surveillance/idwr/provisional",
    
      ZIP_API:
        "https://zipcloud.ibsnet.co.jp/api/search",
    
      // 最新データを何週前まで探すか
      SEARCH_WEEKS: 8,
    
      // CSVキャッシュ
      CACHE_HOURS: 6,
    
      // Widget更新
      REFRESH_MINUTES: 180,
    
      // 表示対象
      TARGET_DISEASES: [
    
        "インフルエンザ",
        "RSウイルス感染症",
        "咽頭結膜熱",
        "A群溶血性レンサ球菌咽頭炎",
        "感染性胃腸炎",
        "水痘",
        "手足口病",
        "伝染性紅斑",
        "突発性発しん",
        "ヘルパンギーナ",
        "流行性耳下腺炎",
        "急性出血性結膜炎",
        "流行性角結膜炎",
        "細菌性髄膜炎",
        "無菌性髄膜炎",
        "マイコプラズマ肺炎",
        "クラミジア肺炎",
        "感染性胃腸炎(ロタウイルス)",
        "COVID-19"
    
      ]
    };
    
    
    // =====================================================
    // パラメーター
    // =====================================================
    
    const rawParam =
      (args.widgetParameter || "").trim();
    
    const zipcode =
      rawParam.replace(/[^0-9]/g, "");
    
    
    // =====================================================
    // FileManager
    // =====================================================
    
    const fm =
      FileManager.local();
    
    const cacheDir =
      fm.joinPath(
        fm.documentsDirectory(),
        "infection-risk-cache"
      );
    
    if (!fm.fileExists(cacheDir)) {
      fm.createDirectory(cacheDir);
    }
    
    
    // =====================================================
    // 色
    // =====================================================
    
    const COLORS = {
    
      bgTop:
        new Color("#111827"),
    
      bgBottom:
        new Color("#05070B"),
    
      text:
        Color.white(),
    
      sub:
        new Color("#9CA3AF"),
    
      red:
        new Color("#FF5A5F"),
    
      orange:
        new Color("#FF9F43"),
    
      yellow:
        new Color("#FFD166"),
    
      green:
        new Color("#53D769"),
    
      line:
        new Color("#FFFFFF", 0.10)
    
    };
    
    
    // =====================================================
    // メイン
    // =====================================================
    
    let widget;
    
    try {
    
      if (!/^\d{7}$/.test(zipcode)) {
    
        throw new Error(
          "郵便番号を7桁で入力してください"
        );
    
      }
    
    
      // -------------------------------------------------
      // 郵便番号 → 都道府県
      // -------------------------------------------------
    
      const location =
        await getLocation(zipcode);
    
      const prefecture =
        location.prefecture;
    
    
      // -------------------------------------------------
      // 最新週
      // -------------------------------------------------
    
      const latest =
        await findLatestWeek();
    
    
      // -------------------------------------------------
      // 前週
      // -------------------------------------------------
    
      const previousWeek =
        getPreviousWeek(
          latest.year,
          latest.week
        );
    
    
      // -------------------------------------------------
      // CSV
      // -------------------------------------------------
    
      const currentCSV =
        latest.csv;
    
      const previousCSV =
        await getWeekCSV(
          previousWeek.year,
          previousWeek.week
        );
    
    
      // -------------------------------------------------
      // CSV解析
      // -------------------------------------------------
    
      const current =
        parseIDWR(
          currentCSV,
          prefecture
        );
    
      const previous =
        previousCSV
          ? parseIDWR(
              previousCSV,
              prefecture
            )
          : {};
    
    
      // -------------------------------------------------
      // ランキング
      // -------------------------------------------------
    
      const ranking =
        createRanking(
          current,
          previous
        );
    
    
      if (ranking.length === 0) {
    
        throw new Error(
          "感染症データを取得できませんでした"
        );
    
      }
    
    
      // -------------------------------------------------
      // Widget
      // -------------------------------------------------
    
      widget =
        createWidget(
          ranking,
          prefecture,
          latest
        );
    
    
      // 更新予定
      widget.refreshAfterDate =
        new Date(
          Date.now() +
          CONFIG.REFRESH_MINUTES *
          60 *
          1000
        );
    
    }
    catch (e) {
    
      widget =
        createErrorWidget(
          e.message
        );
    
    }
    
    
    // =====================================================
    // Widget表示
    // =====================================================
    
    if (config.runsInWidget) {
    
      Script.setWidget(widget);
    
    }
    else {
    
      const family =
        config.widgetFamily ||
        "medium";
    
    
      if (family === "small") {
    
        await widget.presentSmall();
    
      }
      else if (family === "large") {
    
        await widget.presentLarge();
    
      }
      else {
    
        await widget.presentMedium();
    
      }
    
    }
    
    Script.complete();
    
    
    // =====================================================
    // 郵便番号 → 都道府県
    // =====================================================
    
    async function getLocation(zip) {
    
      const cachePath =
        fm.joinPath(
          cacheDir,
          `zip-${zip}.json`
        );
    
    
      // -------------------------------------------------
      // キャッシュ
      // -------------------------------------------------
    
      if (fm.fileExists(cachePath)) {
    
        try {
    
          const data =
            JSON.parse(
              fm.readString(cachePath)
            );
    
    
          if (data.prefecture) {
            return data;
          }
    
        }
        catch (_) {}
    
      }
    
    
      // -------------------------------------------------
      // API
      // -------------------------------------------------
    
      const url =
        `${CONFIG.ZIP_API}?zipcode=${zip}`;
    
    
      const req =
        new Request(url);
    
    
      const json =
        await req.loadJSON();
    
    
      if (
        json.status !== 200 ||
        !json.results ||
        json.results.length === 0
      ) {
    
        throw new Error(
          "郵便番号から地域を取得できませんでした"
        );
    
      }
    
    
      const result = {
    
        prefecture:
          json.results[0].address1,
    
        city:
          json.results[0].address2
    
      };
    
    
      fm.writeString(
        cachePath,
        JSON.stringify(result)
      );
    
    
      return result;
    }
    
    
    // =====================================================
    // 最新週を探す
    // =====================================================
    
    async function findLatestWeek() {
    
      const today =
        new Date();
    
    
      const info =
        getISOWeek(today);
    
    
      let year =
        info.year;
    
      let week =
        info.week;
    
    
      // JIHSは現在週より遅れて公開されるため
      // 現在週から過去へ検索
    
      for (
        let i = 0;
        i < CONFIG.SEARCH_WEEKS;
        i++
      ) {
    
        const csv =
          await getWeekCSV(
            year,
            week
          );
    
    
        if (
          csv &&
          csv.includes("インフルエンザ") &&
          csv.includes("COVID-19")
        ) {
    
          return {
    
            year,
            week,
            csv
    
          };
    
        }
    
    
        const prev =
          getPreviousWeek(
            year,
            week
          );
    
    
        year =
          prev.year;
    
        week =
          prev.week;
    
      }
    
    
      throw new Error(
        "最新のIDWRデータが見つかりません"
      );
    }
    
    
    // =====================================================
    // IDWR CSV取得
    // =====================================================
    
    async function getWeekCSV(
      year,
      week
    ) {
    
      const ww =
        String(week)
          .padStart(2, "0");
    
    
      const cachePath =
        fm.joinPath(
          cacheDir,
          `idwr-${year}-${ww}.csv`
        );
    
    
      // -------------------------------------------------
      // キャッシュ
      // -------------------------------------------------
    
      if (fm.fileExists(cachePath)) {
    
        const mod =
          fm.modificationDate(
            cachePath
          );
    
    
        if (mod) {
    
          const age =
            (
              Date.now() -
              mod.getTime()
            ) /
            3600000;
    
    
          if (
            age <
            CONFIG.CACHE_HOURS
          ) {
    
            return fm.readString(
              cachePath
            );
    
          }
    
        }
    
      }
    
    
      // -------------------------------------------------
      // JIHS
      // -------------------------------------------------
    
      const urls = [
    
        `${CONFIG.JIHS_BASE}/${year}/${ww}/${year}-${ww}-teiten.csv`,
    
        `${CONFIG.JIHS_BASE}/${year}/${ww}/${year}-${ww}-teiten.csv?download=1`
    
      ];
    
    
      for (const url of urls) {
    
        try {
    
          const req =
            new Request(url);
    
    
          req.timeoutInterval =
            15;
    
    
          const data =
            await req.load();
    
    
          if (!data) {
            continue;
          }
    
    
          const text =
            decodeCSV(data);
    
    
          if (
            text &&
            text.includes(
              "インフルエンザ"
            )
          ) {
    
            fm.writeString(
              cachePath,
              text
            );
    
    
            return text;
    
          }
    
        }
        catch (_) {}
    
      }
    
    
      // -------------------------------------------------
      // 通信失敗時は古いキャッシュ
      // -------------------------------------------------
    
      if (fm.fileExists(cachePath)) {
    
        return fm.readString(
          cachePath
        );
    
      }
    
    
      return null;
    }
    
    
    // =====================================================
    // CSV文字コード
    // =====================================================
    
    function decodeCSV(data) {
    
      try {
    
        const text =
          data.toRawString();
    
    
        if (
          text &&
          text.includes(
            "インフルエンザ"
          )
        ) {
    
          return text;
    
        }
    
      }
      catch (_) {}
    
    
      try {
    
        return data.toString();
    
      }
      catch (_) {}
    
    
      return "";
    }
    
    
    // =====================================================
    // CSV解析
    // =====================================================
    
    function parseIDWR(
      csv,
      prefecture
    ) {
    
      const rows =
        parseCSV(csv);
    
    
      if (rows.length < 5) {
        return {};
      }
    
    
      // -------------------------------------------------
      // 感染症名が書かれている行を探す
      // -------------------------------------------------
    
      let diseaseRowIndex =
        -1;
    
    
      for (
        let i = 0;
        i <
        Math.min(
          rows.length,
          10
        );
        i++
      ) {
    
        if (
          rows[i].includes(
            "インフルエンザ"
          )
        ) {
    
          diseaseRowIndex =
            i;
    
          break;
    
        }
    
      }
    
    
      if (diseaseRowIndex < 0) {
        return {};
      }
    
    
      const diseaseRow =
        rows[
          diseaseRowIndex
        ];
    
    
      const typeRow =
        rows[
          diseaseRowIndex + 1
        ];
    
    
      // -------------------------------------------------
      // 都道府県行
      // -------------------------------------------------
    
      const prefRow =
        rows.find(
          row =>
            normalize(row[0]) ===
            normalize(prefecture)
        );
    
    
      if (!prefRow) {
        return {};
      }
    
    
      // -------------------------------------------------
      // 定点当たり報告数
      // -------------------------------------------------
    
      const result = {};
    
      let currentDisease =
        null;
    
    
      for (
        let col = 1;
        col <
        diseaseRow.length;
        col++
      ) {
    
        const disease =
          normalize(
            diseaseRow[col]
          );
    
    
        if (disease) {
    
          currentDisease =
            disease;
    
        }
    
    
        if (!currentDisease) {
          continue;
        }
    
    
        const type =
          normalize(
            typeRow[col]
          );
    
    
        // 定点当たりだけ使用
    
        if (type !== "定当") {
          continue;
        }
    
    
        const value =
          parseNumber(
            prefRow[col]
          );
    
    
        result[
          currentDisease
        ] = value;
    
      }
    
    
      return result;
    }
    
    
    // =====================================================
    // 注目度ランキング
    // =====================================================
    
    function createRanking(
      current,
      previous
    ) {
    
      const items = [];
    
    
      for (
        const originalName
        of CONFIG.TARGET_DISEASES
      ) {
    
        const name =
          normalize(
            originalName
          );
    
    
        const now =
          current[name];
    
    
        if (
          now === undefined ||
          now === null ||
          isNaN(now)
        ) {
    
          continue;
    
        }
    
    
        const prev =
          previous[name];
    
    
        // -------------------------------------------------
        // 前週比
        // -------------------------------------------------
    
        let change =
          null;
    
    
        if (
          prev !== undefined &&
          prev !== null &&
          prev > 0
        ) {
    
          change =
            (
              (now - prev) /
              prev
            ) * 100;
    
        }
        else if (
          now > 0 &&
          prev === 0
        ) {
    
          change = 100;
    
        }
    
    
        // =================================================
        // 現在の報告水準
        // =================================================
    
        let levelScore =
          0;
    
    
        if (now >= 10) {
    
          levelScore = 65;
    
        }
        else if (now >= 5) {
    
          levelScore = 58;
    
        }
        else if (now >= 3) {
    
          levelScore = 50;
    
        }
        else if (now >= 2) {
    
          levelScore = 43;
    
        }
        else if (now >= 1) {
    
          levelScore = 35;
    
        }
        else if (now >= 0.5) {
    
          levelScore = 25;
    
        }
        else if (now >= 0.2) {
    
          levelScore = 16;
    
        }
        else if (now >= 0.1) {
    
          levelScore = 8;
    
        }
        else if (now > 0) {
    
          levelScore = 2;
    
        }
    
    
        // =================================================
        // 増減スコア
        // =================================================
    
        let trendScore =
          0;
    
    
        if (change !== null) {
    
          if (change >= 100) {
    
            trendScore = 45;
    
          }
          else if (change >= 50) {
    
            trendScore = 38;
    
          }
          else if (change >= 25) {
    
            trendScore = 30;
    
          }
          else if (change >= 10) {
    
            trendScore = 22;
    
          }
          else if (change >= 5) {
    
            trendScore = 14;
    
          }
          else if (change > 0) {
    
            trendScore = 8;
    
          }
          else if (change <= -50) {
    
            trendScore = -18;
    
          }
          else if (change <= -25) {
    
            trendScore = -12;
    
          }
          else if (change <= -10) {
    
            trendScore = -7;
    
          }
          else if (change < 0) {
    
            trendScore = -3;
    
          }
    
        }
    
    
        // =================================================
        // 少数報告補正
        //
        // 例:
        // 0.02 → 0.07
        //
        // +250%でも現在の報告水準が低いため
        // 増加率の影響を弱くする
        // =================================================
    
        let trendWeight =
          1;
    
    
        if (now < 0.05) {
    
          trendWeight = 0.10;
    
        }
        else if (now < 0.10) {
    
          trendWeight = 0.20;
    
        }
        else if (now < 0.20) {
    
          trendWeight = 0.35;
    
        }
        else if (now < 0.50) {
    
          trendWeight = 0.60;
    
        }
        else if (now < 1.00) {
    
          trendWeight = 0.80;
    
        }
    
    
        trendScore *=
          trendWeight;
    
    
        // =================================================
        // ごく少数の報告へのペナルティ
        // =================================================
    
        let lowPenalty =
          0;
    
    
        if (now === 0) {
    
          lowPenalty =
            -100;
    
        }
        else if (now < 0.05) {
    
          lowPenalty =
            -35;
    
        }
        else if (now < 0.10) {
    
          lowPenalty =
            -20;
    
        }
        else if (now < 0.20) {
    
          lowPenalty =
            -8;
    
        }
    
    
        // =================================================
        // 最終スコア
        // =================================================
    
        const score =
          levelScore +
          trendScore +
          lowPenalty;
    
    
        items.push({
    
          name:
            displayName(name),
    
          rawName:
            name,
    
          value:
            now,
    
          previous:
            prev,
    
          change,
    
          score,
    
          status:
            getStatus(
              now,
              change
            )
    
        });
    
      }
    
    
      // -------------------------------------------------
      // 注目度順
      // -------------------------------------------------
    
      return items.sort(
        (a, b) => {
    
          if (
            b.score !==
            a.score
          ) {
    
            return (
              b.score -
              a.score
            );
    
          }
    
    
          return (
            b.value -
            a.value
          );
    
        }
      );
    }
    
    
    // =====================================================
    // 状況判定
    // =====================================================
    
    function getStatus(
      value,
      change
    ) {
    
      if (value <= 0) {
    
        return {
    
          text:
            "報告なし",
    
          level:
            0
    
        };
    
      }
    
    
      if (change === null) {
    
        return {
    
          text:
            "報告あり",
    
          level:
            1
    
        };
    
      }
    
    
      if (change >= 50) {
    
        return {
    
          text:
            "急増中",
    
          level:
            4
    
        };
    
      }
    
    
      if (change >= 20) {
    
        return {
    
          text:
            "増加中",
    
          level:
            3
    
        };
    
      }
    
    
      if (change >= 5) {
    
        return {
    
          text:
            "やや増加",
    
          level:
            2
    
        };
    
      }
    
    
      if (change <= -20) {
    
        return {
    
          text:
            "減少中",
    
          level:
            0
    
        };
    
      }
    
    
      if (change < -5) {
    
        return {
    
          text:
            "やや減少",
    
          level:
            0
    
        };
    
      }
    
    
      return {
    
        text:
          "横ばい",
    
        level:
          1
    
      };
    }
    
    
    // =====================================================
    // Widget作成
    // =====================================================
    
    function createWidget(
      ranking,
      prefecture,
      latest
    ) {
    
      const w =
        new ListWidget();
    
    
      // -------------------------------------------------
      // 背景
      // -------------------------------------------------
    
      const gradient =
        new LinearGradient();
    
    
      gradient.colors = [
    
        COLORS.bgTop,
        COLORS.bgBottom
    
      ];
    
    
      gradient.locations = [
    
        0,
        1
    
      ];
    
    
      w.backgroundGradient =
        gradient;
    
    
      // -------------------------------------------------
      // サイズ
      // -------------------------------------------------
    
      const family =
        config.widgetFamily ||
        "medium";
    
    
      // 小:TOP1
    
      if (family === "small") {
    
        return createSmall(
          w,
          ranking[0],
          prefecture,
          latest
        );
    
      }
    
    
      // 大:TOP7
    
      if (family === "large") {
    
        return createList(
          w,
          ranking.slice(
            0,
            7
          ),
          prefecture,
          latest,
          true
        );
    
      }
    
    
      // 中:TOP3
    
      return createList(
        w,
        ranking.slice(
          0,
          3
        ),
        prefecture,
        latest,
        false
      );
    }
    
    
    // =====================================================
    // 小サイズ
    // =====================================================
    
    function createSmall(
      w,
      item,
      prefecture,
      latest
    ) {
    
      w.setPadding(
        14,
        14,
        13,
        14
      );
    
    
      // -------------------------------------------------
      // Header
      // -------------------------------------------------
    
      const header =
        w.addStack();
    
    
      header.centerAlignContent();
    
    
      const icon =
        header.addText("●");
    
    
      icon.font =
        Font.boldSystemFont(11);
    
    
      icon.textColor =
        statusColor(
          item.status.level
        );
    
    
      header.addSpacer(5);
    
    
      const title =
        header.addText(
          "感染症リスク"
        );
    
    
      title.font =
        Font.boldSystemFont(12);
    
    
      title.textColor =
        COLORS.text;
    
    
      w.addSpacer();
    
    
      // -------------------------------------------------
      // 感染症名
      // -------------------------------------------------
    
      const disease =
        w.addText(
          item.name
        );
    
    
      disease.font =
        Font.boldSystemFont(
          item.name.length > 9
            ? 15
            : 18
        );
    
    
      disease.textColor =
        COLORS.text;
    
    
      disease.lineLimit =
        2;
    
    
      w.addSpacer(5);
    
    
      // -------------------------------------------------
      // 状況
      // -------------------------------------------------
    
      const status =
        w.addText(
          item.status.text
        );
    
    
      status.font =
        Font.boldSystemFont(15);
    
    
      status.textColor =
        statusColor(
          item.status.level
        );
    
    
      w.addSpacer(4);
    
    
      // -------------------------------------------------
      // 定点当たり
      // -------------------------------------------------
    
      const value =
        w.addText(
          `${formatNumber(item.value)} / 定点`
        );
    
    
      value.font =
        Font.mediumSystemFont(11);
    
    
      value.textColor =
        COLORS.sub;
    
    
      // -------------------------------------------------
      // 前週比
      // -------------------------------------------------
    
      if (
        item.change !== null
      ) {
    
        w.addSpacer(2);
    
    
        const change =
          w.addText(
            formatChange(
              item.change
            )
          );
    
    
        change.font =
          Font.mediumSystemFont(10);
    
    
        change.textColor =
          changeColor(
            item.change
          );
    
      }
    
    
      w.addSpacer();
    
    
      // -------------------------------------------------
      // Footer
      // -------------------------------------------------
    
      const footer =
        w.addText(
          `${prefecture}|第${latest.week}週`
        );
    
    
      footer.font =
        Font.systemFont(9);
    
    
      footer.textColor =
        COLORS.sub;
    
    
      return w;
    }
    
    
    // =====================================================
    // 中・大サイズ
    // =====================================================
    
    function createList(
      w,
      items,
      prefecture,
      latest,
      large
    ) {
    
      w.setPadding(
    
        large
          ? 18
          : 14,
    
        large
          ? 18
          : 15,
    
        large
          ? 16
          : 13,
    
        large
          ? 18
          : 15
    
      );
    
    
      // -------------------------------------------------
      // Header
      // -------------------------------------------------
    
      const header =
        w.addStack();
    
    
      header.centerAlignContent();
    
    
      const title =
        header.addText(
          "今週の感染症リスク"
        );
    
    
      title.font =
        Font.boldSystemFont(
          large
            ? 17
            : 15
        );
    
    
      title.textColor =
        COLORS.text;
    
    
      header.addSpacer();
    
    
      const place =
        header.addText(
          prefecture
        );
    
    
      place.font =
        Font.mediumSystemFont(
          large
            ? 11
            : 10
        );
    
    
      place.textColor =
        COLORS.sub;
    
    
      w.addSpacer(
        large
          ? 11
          : 8
      );
    
    
      // -------------------------------------------------
      // 感染症一覧
      // -------------------------------------------------
    
      items.forEach(
        (item, index) => {
    
          addDiseaseRow(
            w,
            item,
            large
          );
    
    
          if (
            index <
            items.length - 1
          ) {
    
            w.addSpacer(
              large
                ? 5
                : 4
            );
    
    
            const line =
              w.addStack();
    
    
            line.size =
              new Size(
                0,
                1
              );
    
    
            line.backgroundColor =
              COLORS.line;
    
    
            w.addSpacer(
              large
                ? 5
                : 4
            );
    
          }
    
        }
      );
    
    
      w.addSpacer();
    
    
      // -------------------------------------------------
      // Footer
      // -------------------------------------------------
    
      const footer =
        w.addStack();
    
    
      footer.centerAlignContent();
    
    
      const source =
        footer.addText(
          "JIHS 感染症発生動向調査"
        );
    
    
      source.font =
        Font.systemFont(8);
    
    
      source.textColor =
        COLORS.sub;
    
    
      footer.addSpacer();
    
    
      const week =
        footer.addText(
          `第${latest.week}週`
        );
    
    
      week.font =
        Font.systemFont(8);
    
    
      week.textColor =
        COLORS.sub;
    
    
      return w;
    }
    
    
    // =====================================================
    // 感染症 Row
    // =====================================================
    
    function addDiseaseRow(
      w,
      item,
      large
    ) {
    
      const row =
        w.addStack();
    
    
      row.centerAlignContent();
    
    
      // -------------------------------------------------
      // ●
      // -------------------------------------------------
    
      const dot =
        row.addText("●");
    
    
      dot.font =
        Font.boldSystemFont(
          large
            ? 10
            : 9
        );
    
    
      dot.textColor =
        statusColor(
          item.status.level
        );
    
    
      row.addSpacer(7);
    
    
      // -------------------------------------------------
      // 左側
      // -------------------------------------------------
    
      const left =
        row.addStack();
    
    
      left.layoutVertically();
    
    
      const name =
        left.addText(
          item.name
        );
    
    
      name.font =
        Font.semiboldSystemFont(
          large
            ? 13
            : 11
        );
    
    
      name.textColor =
        COLORS.text;
    
    
      name.lineLimit =
        1;
    
    
      // 大サイズは定点当たりも表示
    
      if (large) {
    
        left.addSpacer(2);
    
    
        const value =
          left.addText(
            `${formatNumber(item.value)} / 定点`
          );
    
    
        value.font =
          Font.systemFont(9);
    
    
        value.textColor =
          COLORS.sub;
    
      }
    
    
      row.addSpacer();
    
    
      // -------------------------------------------------
      // 右側
      // -------------------------------------------------
    
      const right =
        row.addStack();
    
    
      right.layoutVertically();
    
    
      const status =
        right.addText(
          item.status.text
        );
    
    
      status.font =
        Font.boldSystemFont(
          large
            ? 11
            : 10
        );
    
    
      status.textColor =
        statusColor(
          item.status.level
        );
    
    
      status.rightAlignText();
    
    
      if (
        item.change !== null
      ) {
    
        right.addSpacer(2);
    
    
        const change =
          right.addText(
            formatChange(
              item.change
            )
          );
    
    
        change.font =
          Font.systemFont(
            large
              ? 9
              : 8
          );
    
    
        change.textColor =
          changeColor(
            item.change
          );
    
    
        change.rightAlignText();
    
      }
    }
    
    
    // =====================================================
    // エラーWidget
    // =====================================================
    
    function createErrorWidget(
      message
    ) {
    
      const w =
        new ListWidget();
    
    
      w.backgroundColor =
        new Color(
          "#111827"
        );
    
    
      w.setPadding(
        16,
        16,
        16,
        16
      );
    
    
      const icon =
        w.addText("⚠︎");
    
    
      icon.font =
        Font.boldSystemFont(24);
    
    
      icon.textColor =
        COLORS.orange;
    
    
      w.addSpacer(10);
    
    
      const title =
        w.addText(
          "感染症リスク"
        );
    
    
      title.font =
        Font.boldSystemFont(16);
    
    
      title.textColor =
        Color.white();
    
    
      w.addSpacer(6);
    
    
      const text =
        w.addText(
          message
        );
    
    
      text.font =
        Font.systemFont(11);
    
    
      text.textColor =
        COLORS.sub;
    
    
      text.lineLimit =
        4;
    
    
      return w;
    }
    
    
    // =====================================================
    // CSV Parser
    // =====================================================
    
    function parseCSV(text) {
    
      const rows = [];
    
      let row = [];
    
      let field = "";
    
      let quote = false;
    
    
      for (
        let i = 0;
        i < text.length;
        i++
      ) {
    
        const c =
          text[i];
    
    
        if (quote) {
    
          if (c === '"') {
    
            if (
              text[i + 1] === '"'
            ) {
    
              field += '"';
    
              i++;
    
            }
            else {
    
              quote = false;
    
            }
    
          }
          else {
    
            field += c;
    
          }
    
        }
        else {
    
          if (c === '"') {
    
            quote = true;
    
          }
          else if (c === ",") {
    
            row.push(field);
    
            field = "";
    
          }
          else if (c === "\n") {
    
            row.push(field);
    
            rows.push(row);
    
            row = [];
    
            field = "";
    
          }
          else if (c !== "\r") {
    
            field += c;
    
          }
    
        }
    
      }
    
    
      if (
        field.length ||
        row.length
      ) {
    
        row.push(field);
    
        rows.push(row);
    
      }
    
    
      return rows;
    }
    
    
    // =====================================================
    // ISO週番号
    // =====================================================
    
    function getISOWeek(date) {
    
      const d =
        new Date(
          Date.UTC(
            date.getFullYear(),
            date.getMonth(),
            date.getDate()
          )
        );
    
    
      const day =
        d.getUTCDay() ||
        7;
    
    
      d.setUTCDate(
        d.getUTCDate() +
        4 -
        day
      );
    
    
      const yearStart =
        new Date(
          Date.UTC(
            d.getUTCFullYear(),
            0,
            1
          )
        );
    
    
      const week =
        Math.ceil(
          (
            (
              d -
              yearStart
            ) /
            86400000 +
            1
          ) /
          7
        );
    
    
      return {
    
        year:
          d.getUTCFullYear(),
    
        week
    
      };
    }
    
    
    // =====================================================
    // 前週
    // =====================================================
    
    function getPreviousWeek(
      year,
      week
    ) {
    
      if (week > 1) {
    
        return {
    
          year,
    
          week:
            week - 1
    
        };
    
      }
    
    
      const d =
        new Date(
          year - 1,
          11,
          28
        );
    
    
      const info =
        getISOWeek(d);
    
    
      return {
    
        year:
          year - 1,
    
        week:
          info.week
    
      };
    }
    
    
    // =====================================================
    // 感染症表示名
    // =====================================================
    
    function displayName(name) {
    
      const map = {
    
        "RSウイルス感染症":
          "RSウイルス",
    
        "A群溶血性レンサ球菌咽頭炎":
          "溶連菌",
    
        "感染性胃腸炎(ロタウイルス)":
          "ロタウイルス",
    
        "COVID-19":
          "新型コロナ",
    
        "流行性耳下腺炎":
          "おたふくかぜ",
    
        "マイコプラズマ肺炎":
          "マイコプラズマ肺炎",
    
        "急性出血性結膜炎":
          "急性出血性結膜炎",
    
        "流行性角結膜炎":
          "流行性角結膜炎"
    
      };
    
    
      return (
        map[name] ||
        name
      );
    }
    
    
    // =====================================================
    // 状況色
    // =====================================================
    
    function statusColor(level) {
    
      if (level >= 4) {
    
        return COLORS.red;
    
      }
    
    
      if (level === 3) {
    
        return COLORS.orange;
    
      }
    
    
      if (level === 2) {
    
        return COLORS.yellow;
    
      }
    
    
      return COLORS.green;
    }
    
    
    // =====================================================
    // 前週比色
    // =====================================================
    
    function changeColor(value) {
    
      if (value >= 20) {
    
        return COLORS.red;
    
      }
    
    
      if (value > 0) {
    
        return COLORS.orange;
    
      }
    
    
      if (value < 0) {
    
        return COLORS.green;
    
      }
    
    
      return COLORS.sub;
    }
    
    
    // =====================================================
    // 数値表示
    // =====================================================
    
    function formatNumber(value) {
    
      if (value >= 10) {
    
        return value.toFixed(1);
    
      }
    
    
      return value.toFixed(2);
    }
    
    
    // =====================================================
    // 前週比表示
    // =====================================================
    
    function formatChange(value) {
    
      const rounded =
        Math.round(
          Math.abs(value)
        );
    
    
      if (value > 0) {
    
        return (
          `↑ 前週 +${rounded}%`
        );
    
      }
    
    
      if (value < 0) {
    
        return (
          `↓ 前週 -${rounded}%`
        );
    
      }
    
    
      return "→ 前週 ±0%";
    }
    
    
    // =====================================================
    // 数値変換
    // =====================================================
    
    function parseNumber(value) {
    
      if (
        value === undefined ||
        value === null
      ) {
    
        return 0;
    
      }
    
    
      const n =
        parseFloat(
          String(value)
            .replace(/,/g, "")
            .trim()
        );
    
    
      return isNaN(n)
        ? 0
        : n;
    }
    
    
    // =====================================================
    // 文字列正規化
    // =====================================================
    
    function normalize(value) {
    
      if (
        value === undefined ||
        value === null
      ) {
    
        return "";
    
      }
    
    
      return String(value)
        .replace(
          /^\uFEFF/,
          ""
        )
        .replace(
          /\u3000/g,
          " "
        )
        .trim();
    }
  3. ホーム画面に追加して設定する

    iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。

    追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。

    「Parameter」に確認したい地域の7桁の郵便番号を入力します。例:1000001。ハイフンあり・なしのどちらでも使えます。

    編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。

「感染症リスク」について

本ウィジェットのランキングや「急増中」「増加中」などの表示は、公的な警報・注意報を示すものではありません。
JIHSが公開している定点当たり報告数と前週からの増減をもとに、現在の状況を分かりやすく確認するために独自に算出しています。
特に報告数が少ない感染症では増減率が大きくなりやすいため、現在の報告水準も考慮してランキングを調整しています。

データ提供
国立健康危機管理研究機構(JIHS)感染症情報提供サイト

動作確認・使用結果

確認した端末 iPhone 17 ProMAX
iOS iOS27
Scriptable 1.7.19
確認したサイズ 小・中・大
バージョン 1.0.0
動作確認日 2026-09-26

表示・使用時のポイント

郵便番号から地域を特定し、週報の報告値を取得します。表示するリスクは報告数や変化を使った独自の目安です。

更新履歴

1.0.0

  • 初回リリース

よくあるエラーと対処

確認すること
表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
地域が取得できない
7桁の郵便番号をParameterに入力してください。
最新の週が出ない
週報は毎日更新ではありません。公開済みの対象週を確認してください。
前週差が出ない
比較対象の週のデータが取得できない場合があります。

注意事項・制限

  • 個人の感染確率や診断を示すものではありません。
  • 速報値は後から修正される場合があります。対象週と地域を確認してください。
  • ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。

データ提供・参考資料

国立健康危機管理研究機構(JIHS)感染症発生動向調査・週報速報:感染症の届出・集計情報。
zipcloud:郵便番号から都道府県を特定します。

SHARE

この記事をシェア