設定難易度

世界でいちばん暑い場所、いちばん寒い場所はどこ?
日本の最高・最低気温ランキングを作っていて、「これ、世界まで広げたらもっと面白いんじゃない?」と思ったのが、このウィジェットを作ったきっかけです。

気象庁の「世界の天候データツール(ClimatView)」を利用して、世界各地の最高・最低気温をランキング形式で表示するScriptableウィジェットです。

「最高」「最低」を切り替えるだけで、その日の世界の気温ランキングをホーム画面から確認できます。

前日の同じ観測地点との気温差も表示するので、「昨日より何℃暑い・寒い?」もひと目でわかります。

※データはリアルタイムではなく、気象庁から公開された最新の観測日データを使用しています。

ウィジェットの表示内容

気象庁ClimatViewの公開データから、対象日の最高・最低気温を比較します。

サイズごとの表示

  • 小:最高気温または最低気温の1位を表示します。
  • 中:上位3地点、大:上位8地点を表示します。
  • 前日と比較できる地点では、気温差も確認できます。

設定手順

上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。

  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: deep-blue; icon-glyph: globe;
    // Version: 1.0.0
    
    // =====================================================
    // 世界の気温ランキング - Scriptable Widget
    //
    // Parameter:
    //   最高 → 日最高気温の高い方から
    //   最低 → 日最低気温の低い方から
    //
    // 未入力 → 最高
    //
    // 小:1位
    // 中:TOP3
    // 大:TOP8
    //
    // 気温右側:同一観測地点の前日差
    //
    // データ:気象庁
    // 「世界の天候データツール(ClimatView 日別値)」
    //
    // ※更新タイミングにより前日以前の観測値を表示します。
    // =====================================================
    
    
    // =====================================================
    // Parameter
    // =====================================================
    
    const rawParam =
      (args.widgetParameter || "").trim();
    
    const MODE =
      rawParam === "最低" ? "最低" : "最高";
    
    
    // =====================================================
    // 設定
    // =====================================================
    
    const CONFIG = {
    
      BASE_URL:
    	"https://www.data.jma.go.jp/cpd/monitor/dailyview/all_download_d.php",
    
      PAGE_URL:
    	"https://www.data.jma.go.jp/cpd/monitor/dailyview/index.php",
    
      MAX_LOOKBACK: 7,
    
      REFRESH_MINUTES: 60
    
    };
    
    
    // =====================================================
    // モード設定
    // =====================================================
    
    const SETTINGS = {
    
      最高: {
    
    	title:
    	  "最高気温ランキング",
    
    	shortTitle:
    	  "最高気温",
    
    	rank1:
    	  new Color("#FF5A5F"),
    
    	rank2:
    	  new Color("#FF9F43"),
    
    	rank3:
    	  new Color("#FFD166")
    
      },
    
      最低: {
    
    	title:
    	  "最低気温ランキング",
    
    	shortTitle:
    	  "最低気温",
    
    	rank1:
    	  new Color("#5AC8FA"),
    
    	rank2:
    	  new Color("#64D2FF"),
    
    	rank3:
    	  new Color("#A7E4FF")
    
      }
    
    };
    
    const SETTING =
      SETTINGS[MODE];
    
    
    // =====================================================
    // カラー
    // =====================================================
    
    const COLOR = {
    
      bgTop:
    	new Color("#181818"),
    
      bgBottom:
    	new Color("#080808"),
    
      text:
    	Color.white(),
    
      subText:
    	new Color("#999999")
    
    };
    
    
    // =====================================================
    // 日付
    // =====================================================
    
    function addDays(date, days) {
    
      const d =
    	new Date(date);
    
      d.setDate(
    	d.getDate() + days
      );
    
      return d;
    }
    
    
    function formatDate(date) {
    
      return (
    	`${date.getMonth() + 1}/` +
    	`${date.getDate()} 観測`
      );
    }
    
    
    // =====================================================
    // 背景
    // =====================================================
    
    function setBackground(widget) {
    
      const gradient =
    	new LinearGradient();
    
      gradient.colors = [
    	COLOR.bgTop,
    	COLOR.bgBottom
      ];
    
      gradient.locations = [
    	0,
    	1
      ];
    
      widget.backgroundGradient =
    	gradient;
    }
    
    
    // =====================================================
    // 順位カラー
    // =====================================================
    
    function rankColor(rank) {
    
      if (rank === 1) {
    	return SETTING.rank1;
      }
    
      if (rank === 2) {
    	return SETTING.rank2;
      }
    
      if (rank === 3) {
    	return SETTING.rank3;
      }
    
      return COLOR.text;
    }
    
    
    // =====================================================
    // CSV取得
    // =====================================================
    
    async function fetchDailyData(date) {
    
      const y =
    	date.getFullYear();
    
      const m =
    	date.getMonth() + 1;
    
      const d =
    	date.getDate();
    
    
      const url =
    	`${CONFIG.BASE_URL}?y=${y}&m=${m}&d=${d}`;
    
    
      try {
    
    	const request =
    	  new Request(url);
    
    
    	request.headers = {
    
    	  "User-Agent":
    		"Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)"
    
    	};
    
    
    	request.timeoutInterval =
    	  20;
    
    
    	const data =
    	  await request.load();
    
    
    	if (
    	  !data ||
    	  data.length < 10000
    	) {
    
    	  return null;
    	}
    
    
    	return {
    
    	  data,
    	  date
    
    	};
    
      }
    
      catch (error) {
    
    	console.log(
    	  `取得失敗 ${y}/${m}/${d}`
    	);
    
    	return null;
      }
    }
    
    
    // =====================================================
    // 最新データ取得
    // =====================================================
    
    async function fetchLatestData() {
    
      const now =
    	new Date();
    
    
      for (
    	let i = 1;
    	i <= CONFIG.MAX_LOOKBACK;
    	i++
      ) {
    
    	const date =
    	  addDays(
    		now,
    		-i
    	  );
    
    
    	const result =
    	  await fetchDailyData(
    		date
    	  );
    
    
    	if (result) {
    
    	  return result;
    	}
      }
    
    
      throw new Error(
    	"最新データを取得できませんでした"
      );
    }
    
    
    // =====================================================
    // Shift_JIS → Unicode
    // =====================================================
    
    async function decodeSJIS(data) {
    
      const base64 =
    	data.toBase64String();
    
    
      const web =
    	new WebView();
    
    
      const js = `
    
    	const base64 = "${base64}";
    
    	const binary =
    	  atob(base64);
    
    	const bytes =
    	  new Uint8Array(
    		binary.length
    	  );
    
    	for (
    	  let i = 0;
    	  i < binary.length;
    	  i++
    	) {
    
    	  bytes[i] =
    		binary.charCodeAt(i);
    	}
    
    	const decoder =
    	  new TextDecoder(
    		"shift_jis"
    	  );
    
    	completion(
    	  decoder.decode(bytes)
    	);
    
      `;
    
    
      return await web.evaluateJavaScript(
    	js,
    	true
      );
    }
    
    
    // =====================================================
    // CSV 1行解析
    // =====================================================
    
    function parseCSVLine(line) {
    
      const result = [];
    
      let current = "";
      let quoted = false;
    
    
      for (
    	let i = 0;
    	i < line.length;
    	i++
      ) {
    
    	const c =
    	  line[i];
    
    
    	if (c === '"') {
    
    	  if (
    		quoted &&
    		line[i + 1] === '"'
    	  ) {
    
    		current += '"';
    
    		i++;
    
    	  }
    
    	  else {
    
    		quoted =
    		  !quoted;
    	  }
    
    	}
    
    	else if (
    	  c === "," &&
    	  !quoted
    	) {
    
    	  result.push(
    		current
    	  );
    
    	  current = "";
    
    	}
    
    	else {
    
    	  current += c;
    	}
      }
    
    
      result.push(
    	current
      );
    
    
      return result;
    }
    
    
    // =====================================================
    // 文字整理
    // =====================================================
    
    function cleanText(value) {
    
      return String(
    	value || ""
      )
    
    	.replace(
    	  /^"|"$/g,
    	  ""
    	)
    
    	.trim();
    }
    
    
    // =====================================================
    // 国名整理
    // =====================================================
    
    function cleanCountry(value) {
    
      const country =
    	cleanText(value);
    
    
      // 南極観測基地
      if (
    	/^STATIONS OPERATED BY/i.test(
    	  country
    	)
      ) {
    
    	return "南極";
      }
    
    
      // 英語国名 → 日本語
      const map = {
    
    	"JAPAN":
    	  "日本",
    
    	"CHINA":
    	  "中国",
    
    	"REPUBLIC OF KOREA":
    	  "韓国",
    
    	"KOREA":
    	  "韓国",
    
    	"INDIA":
    	  "インド",
    
    	"PAKISTAN":
    	  "パキスタン",
    
    	"THAILAND":
    	  "タイ",
    
    	"VIET NAM":
    	  "ベトナム",
    
    	"VIETNAM":
    	  "ベトナム",
    
    	"MALAYSIA":
    	  "マレーシア",
    
    	"SINGAPORE":
    	  "シンガポール",
    
    	"INDONESIA":
    	  "インドネシア",
    
    	"PHILIPPINES":
    	  "フィリピン",
    
    	"SAUDI ARABIA":
    	  "サウジアラビア",
    
    	"KUWAIT":
    	  "クウェート",
    
    	"IRAQ":
    	  "イラク",
    
    	"IRAN":
    	  "イラン",
    
    	"UNITED ARAB EMIRATES":
    	  "アラブ首長国連邦",
    
    	"QATAR":
    	  "カタール",
    
    	"BAHRAIN":
    	  "バーレーン",
    
    	"OMAN":
    	  "オマーン",
    
    	"ISRAEL":
    	  "イスラエル",
    
    	"UNITED STATES":
    	  "アメリカ合衆国",
    
    	"UNITED STATES OF AMERICA":
    	  "アメリカ合衆国",
    
    	"CANADA":
    	  "カナダ",
    
    	"MEXICO":
    	  "メキシコ",
    
    	"BRAZIL":
    	  "ブラジル",
    
    	"ARGENTINA":
    	  "アルゼンチン",
    
    	"CHILE":
    	  "チリ",
    
    	"PERU":
    	  "ペルー",
    
    	"COLOMBIA":
    	  "コロンビア",
    
    	"UNITED KINGDOM":
    	  "イギリス",
    
    	"FRANCE":
    	  "フランス",
    
    	"GERMANY":
    	  "ドイツ",
    
    	"ITALY":
    	  "イタリア",
    
    	"SPAIN":
    	  "スペイン",
    
    	"PORTUGAL":
    	  "ポルトガル",
    
    	"NETHERLANDS":
    	  "オランダ",
    
    	"BELGIUM":
    	  "ベルギー",
    
    	"SWITZERLAND":
    	  "スイス",
    
    	"AUSTRIA":
    	  "オーストリア",
    
    	"NORWAY":
    	  "ノルウェー",
    
    	"SWEDEN":
    	  "スウェーデン",
    
    	"FINLAND":
    	  "フィンランド",
    
    	"DENMARK":
    	  "デンマーク",
    
    	"ICELAND":
    	  "アイスランド",
    
    	"POLAND":
    	  "ポーランド",
    
    	"UKRAINE":
    	  "ウクライナ",
    
    	"RUSSIAN FEDERATION":
    	  "ロシア",
    
    	"RUSSIA":
    	  "ロシア",
    
    	"TURKEY":
    	  "トルコ",
    
    	"EGYPT":
    	  "エジプト",
    
    	"LIBYA":
    	  "リビア",
    
    	"ALGERIA":
    	  "アルジェリア",
    
    	"MOROCCO":
    	  "モロッコ",
    
    	"SOUTH AFRICA":
    	  "南アフリカ",
    
    	"AUSTRALIA":
    	  "オーストラリア",
    
    	"NEW ZEALAND":
    	  "ニュージーランド"
    
      };
    
    
      const upper =
    	country.toUpperCase();
    
    
      if (
    	map[upper]
      ) {
    
    	return map[upper];
      }
    
    
      return country;
    }
    
    
    // =====================================================
    // 気温
    // =====================================================
    
    function parseTemperature(value) {
    
      if (!value) {
    
    	return null;
      }
    
    
      const cleaned =
    	String(value)
    
    	  .replace(
    		/"/g,
    		""
    	  )
    
    	  .trim();
    
    
      if (
    	cleaned === "" ||
    	cleaned === "-" ||
    	cleaned === "///" ||
    	cleaned === "×"
      ) {
    
    	return null;
      }
    
    
      const number =
    	parseFloat(
    	  cleaned
    	);
    
    
      return Number.isFinite(
    	number
      )
    	? number
    	: null;
    }
    
    
    // =====================================================
    // CSV解析
    // =====================================================
    
    function parseCSV(text) {
    
      const lines =
    	text
    
    	  .replace(
    		/\r/g,
    		""
    	  )
    
    	  .split("\n")
    
    	  .filter(
    		line =>
    		  line.trim()
    	  );
    
    
      const rows = [];
    
    
      for (
    	let i = 1;
    	i < lines.length;
    	i++
      ) {
    
    	const cols =
    	  parseCSVLine(
    		lines[i]
    	  );
    
    
    	if (
    	  cols.length < 8
    	) {
    
    	  continue;
    	}
    
    
    	const max =
    	  parseTemperature(
    		cols[6]
    	  );
    
    
    	const min =
    	  parseTemperature(
    		cols[7]
    	  );
    
    
    	if (
    	  max === null &&
    	  min === null
    	) {
    
    	  continue;
    	}
    
    
    	rows.push({
    
    	  id:
    		cleanText(
    		  cols[0]
    		),
    
    	  station:
    		cleanText(
    		  cols[1]
    		),
    
    	  country:
    		cleanCountry(
    		  cols[2]
    		),
    
    	  max,
    
    	  min,
    
    	  diff:
    		null
    
    	});
      }
    
    
      return rows;
    }
    
    
    // =====================================================
    // 国旗
    // =====================================================
    
    function countryFlag(country) {
    
      const map = {
    
    	"日本": "🇯🇵",
    	"中国": "🇨🇳",
    	"韓国": "🇰🇷",
    	"北朝鮮": "🇰🇵",
    	"台湾": "🇹🇼",
    	"モンゴル": "🇲🇳",
    
    	"インド": "🇮🇳",
    	"パキスタン": "🇵🇰",
    	"バングラデシュ": "🇧🇩",
    	"ネパール": "🇳🇵",
    	"ブータン": "🇧🇹",
    	"スリランカ": "🇱🇰",
    
    	"タイ": "🇹🇭",
    	"ベトナム": "🇻🇳",
    	"ラオス": "🇱🇦",
    	"カンボジア": "🇰🇭",
    	"ミャンマー": "🇲🇲",
    	"マレーシア": "🇲🇾",
    	"シンガポール": "🇸🇬",
    	"インドネシア": "🇮🇩",
    	"フィリピン": "🇵🇭",
    
    	"サウジアラビア": "🇸🇦",
    	"クウェート": "🇰🇼",
    	"イラク": "🇮🇶",
    	"イラン": "🇮🇷",
    	"アラブ首長国連邦": "🇦🇪",
    	"カタール": "🇶🇦",
    	"バーレーン": "🇧🇭",
    	"オマーン": "🇴🇲",
    	"イエメン": "🇾🇪",
    	"イスラエル": "🇮🇱",
    	"ヨルダン": "🇯🇴",
    	"レバノン": "🇱🇧",
    	"シリア": "🇸🇾",
    
    	"アメリカ合衆国": "🇺🇸",
    	"米国": "🇺🇸",
    	"カナダ": "🇨🇦",
    	"メキシコ": "🇲🇽",
    
    	"ブラジル": "🇧🇷",
    	"アルゼンチン": "🇦🇷",
    	"パラグアイ": "🇵🇾",
    	"ウルグアイ": "🇺🇾",
    	"チリ": "🇨🇱",
    	"ペルー": "🇵🇪",
    	"ボリビア": "🇧🇴",
    	"コロンビア": "🇨🇴",
    	"ベネズエラ": "🇻🇪",
    	"エクアドル": "🇪🇨",
    
    	"イギリス": "🇬🇧",
    	"アイルランド": "🇮🇪",
    	"フランス": "🇫🇷",
    	"ドイツ": "🇩🇪",
    	"イタリア": "🇮🇹",
    	"スペイン": "🇪🇸",
    	"ポルトガル": "🇵🇹",
    	"オランダ": "🇳🇱",
    	"ベルギー": "🇧🇪",
    	"スイス": "🇨🇭",
    	"オーストリア": "🇦🇹",
    
    	"ノルウェー": "🇳🇴",
    	"スウェーデン": "🇸🇪",
    	"フィンランド": "🇫🇮",
    	"デンマーク": "🇩🇰",
    	"アイスランド": "🇮🇸",
    
    	"ポーランド": "🇵🇱",
    	"チェコ": "🇨🇿",
    	"スロバキア": "🇸🇰",
    	"ハンガリー": "🇭🇺",
    	"ルーマニア": "🇷🇴",
    	"ブルガリア": "🇧🇬",
    	"ギリシャ": "🇬🇷",
    
    	"ウクライナ": "🇺🇦",
    	"ベラルーシ": "🇧🇾",
    	"ロシア": "🇷🇺",
    	"トルコ": "🇹🇷",
    
    	"エジプト": "🇪🇬",
    	"リビア": "🇱🇾",
    	"チュニジア": "🇹🇳",
    	"アルジェリア": "🇩🇿",
    	"モロッコ": "🇲🇦",
    
    	"スーダン": "🇸🇩",
    	"エチオピア": "🇪🇹",
    	"ケニア": "🇰🇪",
    	"タンザニア": "🇹🇿",
    	"ナイジェリア": "🇳🇬",
    	"ガーナ": "🇬🇭",
    
    	"南アフリカ": "🇿🇦",
    	"ナミビア": "🇳🇦",
    	"ボツワナ": "🇧🇼",
    	"ジンバブエ": "🇿🇼",
    	"ザンビア": "🇿🇲",
    	"モザンビーク": "🇲🇿",
    	"マダガスカル": "🇲🇬",
    
    	"オーストラリア": "🇦🇺",
    	"ニュージーランド": "🇳🇿",
    	"パプアニューギニア": "🇵🇬",
    	"フィジー": "🇫🇯",
    
    	"南極": "🌐"
    
      };
    
    
      if (
    	map[country]
      ) {
    
    	return map[country];
      }
    
    
      for (
    	const key of Object.keys(map)
      ) {
    
    	if (
    	  country.includes(key)
    	) {
    
    	  return map[key];
    	}
      }
    
    
      return "🌐";
    }
    
    
    // =====================================================
    // 前日差
    // =====================================================
    
    function attachPreviousDiff(
      currentRows,
      previousRows
    ) {
    
      const key =
    	MODE === "最低"
    	  ? "min"
    	  : "max";
    
    
      const previousMap =
    	new Map();
    
    
      for (
    	const row of previousRows
      ) {
    
    	if (!row.id) {
    	  continue;
    	}
    
    
    	previousMap.set(
    	  row.id,
    	  row
    	);
      }
    
    
      for (
    	const row of currentRows
      ) {
    
    	const previous =
    	  previousMap.get(
    		row.id
    	  );
    
    
    	if (
    	  !previous ||
    	  row[key] === null ||
    	  previous[key] === null
    	) {
    
    	  row.diff =
    		null;
    
    	  continue;
    	}
    
    
    	row.diff =
    	  row[key] -
    	  previous[key];
      }
    
    
      return currentRows;
    }
    
    
    // =====================================================
    // 前日差表示
    // =====================================================
    
    function formatDiff(diff) {
    
      if (
    	diff === null ||
    	!Number.isFinite(diff)
      ) {
    
    	return "—";
      }
    
    
      if (
    	Math.abs(diff) < 0.05
      ) {
    
    	return "±0.0°";
      }
    
    
      if (
    	diff > 0
      ) {
    
    	return (
    	  `+${diff.toFixed(1)}°`
    	);
      }
    
    
      return (
    	`${diff.toFixed(1)}°`
      );
    }
    
    
    // =====================================================
    // ランキング
    // =====================================================
    
    function makeRanking(rows) {
    
      const key =
    	MODE === "最低"
    	  ? "min"
    	  : "max";
    
    
      return rows
    
    	.filter(
    	  row =>
    		row[key] !== null
    	)
    
    	.sort(
    	  (a, b) => {
    
    		if (
    		  MODE === "最低"
    		) {
    
    		  return (
    			a[key] -
    			b[key]
    		  );
    		}
    
    
    		return (
    		  b[key] -
    		  a[key]
    		);
    	  }
    	);
    }
    
    
    // =====================================================
    // 小サイズ
    // =====================================================
    
    function createSmall(
      data,
      date
    ) {
    
      const widget =
    	new ListWidget();
    
    
      setBackground(widget);
    
    
      widget.setPadding(
    	15,
    	15,
    	15,
    	15
      );
    
    
      const item =
    	data[0];
    
    
      const key =
    	MODE === "最低"
    	  ? "min"
    	  : "max";
    
    
      // タイトル
    
      const title =
    	widget.addText(
    	  `世界の${SETTING.shortTitle}`
    	);
    
    
      title.font =
    	Font.boldSystemFont(12);
    
    
      title.textColor =
    	COLOR.subText;
    
    
      widget.addSpacer();
    
    
      // 世界1位
    
      const rank =
    	widget.addText(
    	  "世界 1位"
    	);
    
    
      rank.font =
    	Font.boldSystemFont(15);
    
    
      rank.textColor =
    	SETTING.rank1;
    
    
      widget.addSpacer(3);
    
    
      // 地点
    
      const placeRow =
    	widget.addStack();
    
    
      placeRow.centerAlignContent();
    
    
      const flag =
    	placeRow.addText(
    	  countryFlag(
    		item.country
    	  )
    	);
    
    
      flag.font =
    	Font.systemFont(17);
    
    
      placeRow.addSpacer(6);
    
    
      const place =
    	placeRow.addText(
    	  item.station
    	);
    
    
      place.font =
    	Font.boldSystemFont(21);
    
    
      place.textColor =
    	COLOR.text;
    
    
      place.minimumScaleFactor =
    	0.5;
    
    
      place.lineLimit =
    	1;
    
    
      // 国名
    
      const country =
    	widget.addText(
    	  item.country
    	);
    
    
      country.font =
    	Font.systemFont(10);
    
    
      country.textColor =
    	COLOR.subText;
    
    
      country.lineLimit =
    	1;
    
    
      widget.addSpacer(5);
    
    
      // 気温+前日差
    
      const tempRow =
    	widget.addStack();
    
    
      tempRow.bottomAlignContent();
    
    
      const temp =
    	tempRow.addText(
    	  `${item[key].toFixed(1)}℃`
    	);
    
    
      temp.font =
    	Font.boldSystemFont(29);
    
    
      temp.textColor =
    	SETTING.rank1;
    
    
      temp.minimumScaleFactor =
    	0.6;
    
    
      tempRow.addSpacer(7);
    
    
      const diff =
    	tempRow.addText(
    	  formatDiff(
    		item.diff
    	  )
    	);
    
    
      diff.font =
    	Font.systemFont(9);
    
    
      diff.textColor =
    	COLOR.subText;
    
    
      widget.addSpacer();
    
    
      // 観測日
    
      const observation =
    	widget.addText(
    	  formatDate(
    		date
    	  )
    	);
    
    
      observation.font =
    	Font.systemFont(8);
    
    
      observation.textColor =
    	COLOR.subText;
    
    
      widget.url =
    	CONFIG.PAGE_URL;
    
    
      return widget;
    }
    
    
    // =====================================================
    // ランキング行
    // 中・大共通
    // =====================================================
    
    function addRankingRow(
      widget,
      item,
      rankNumber,
      options = {}
    ) {
    
      const key =
    	MODE === "最低"
    	  ? "min"
    	  : "max";
    
    
      const rankFont =
    	options.rankFont || 18;
    
      const flagFont =
    	options.flagFont || 15;
    
      const placeFont =
    	options.placeFont || 15;
    
      const countryFont =
    	options.countryFont || 8;
    
      const tempFont =
    	options.tempFont || 19;
    
      const diffFont =
    	options.diffFont || 9;
    
      const rankWidth =
    	options.rankWidth || 25;
    
    
      const row =
    	widget.addStack();
    
    
      row.centerAlignContent();
    
    
      // 順位
    
      const rank =
    	row.addText(
    	  String(rankNumber)
    	);
    
    
      rank.font =
    	Font.boldSystemFont(
    	  rankFont
    	);
    
    
      rank.textColor =
    	rankColor(
    	  rankNumber
    	);
    
    
      rank.size =
    	new Size(
    	  rankWidth,
    	  20
    	);
    
    
      row.addSpacer(5);
    
    
      // 国旗
    
      const flag =
    	row.addText(
    	  countryFlag(
    		item.country
    	  )
    	);
    
    
      flag.font =
    	Font.systemFont(
    	  flagFont
    	);
    
    
      row.addSpacer(7);
    
    
      // 地点+国名
    
      const location =
    	row.addStack();
    
    
      location.layoutVertically();
    
    
      const place =
    	location.addText(
    	  item.station
    	);
    
    
      place.font =
    	Font.boldSystemFont(
    	  placeFont
    	);
    
    
      place.textColor =
    	COLOR.text;
    
    
      place.minimumScaleFactor =
    	0.5;
    
    
      place.lineLimit =
    	1;
    
    
      const country =
    	location.addText(
    	  item.country
    	);
    
    
      country.font =
    	Font.systemFont(
    	  countryFont
    	);
    
    
      country.textColor =
    	COLOR.subText;
    
    
      country.minimumScaleFactor =
    	0.5;
    
    
      country.lineLimit =
    	1;
    
    
      row.addSpacer();
    
    
      // 気温
    
      const temp =
    	row.addText(
    	  `${item[key].toFixed(1)}°`
    	);
    
    
      temp.font =
    	Font.boldSystemFont(
    	  tempFont
    	);
    
    
      temp.textColor =
    	rankColor(
    	  rankNumber
    	);
    
    
      temp.lineLimit =
    	1;
    
    
      // 前日差
    
      row.addSpacer(7);
    
    
      const diff =
    	row.addText(
    	  formatDiff(
    		item.diff
    	  )
    	);
    
    
      diff.font =
    	Font.systemFont(
    	  diffFont
    	);
    
    
      diff.textColor =
    	COLOR.subText;
    
    
      diff.lineLimit =
    	1;
    
    
      return row;
    }
    
    
    // =====================================================
    // 中サイズ
    // TOP3
    // =====================================================
    
    function createMedium(
      data,
      date
    ) {
    
      const widget =
    	new ListWidget();
    
    
      setBackground(widget);
    
    
      widget.setPadding(
    	14,
    	16,
    	12,
    	16
      );
    
    
      // ヘッダー
    
      const header =
    	widget.addStack();
    
    
      header.centerAlignContent();
    
    
      const title =
    	header.addText(
    	  `世界の${SETTING.title}`
    	);
    
    
      title.font =
    	Font.boldSystemFont(14);
    
    
      title.textColor =
    	COLOR.text;
    
    
      header.addSpacer();
    
    
      const observation =
    	header.addText(
    	  formatDate(
    		date
    	  )
    	);
    
    
      observation.font =
    	Font.systemFont(8);
    
    
      observation.textColor =
    	COLOR.subText;
    
    
      widget.addSpacer(9);
    
    
      // TOP3
    
      for (
    	let i = 0;
    	i <
    	  Math.min(
    		3,
    		data.length
    	  );
    	i++
      ) {
    
    	addRankingRow(
    
    	  widget,
    
    	  data[i],
    
    	  i + 1,
    
    	  {
    		rankFont: 18,
    		flagFont: 15,
    		placeFont: 15,
    		countryFont: 8,
    		tempFont: 19,
    		diffFont: 9,
    		rankWidth: 25
    	  }
    
    	);
    
    
    	if (
    	  i < 2
    	) {
    
    	  widget.addSpacer(7);
    	}
      }
    
    
      widget.url =
    	CONFIG.PAGE_URL;
    
    
      return widget;
    }
    
    
    // =====================================================
    // 大サイズ
    // 中サイズと同じデザインでTOP7
    // =====================================================
    
    function createLarge(
      data,
      date
    ) {
    
      const widget =
    	new ListWidget();
    
    
      setBackground(widget);
    
    
      widget.setPadding(
    	16,
    	18,
    	14,
    	18
      );
    
    
      // ヘッダー
    
      const header =
    	widget.addStack();
    
    
      header.centerAlignContent();
    
    
      const title =
    	header.addText(
    	  `世界の${SETTING.title}`
    	);
    
    
      title.font =
    	Font.boldSystemFont(16);
    
    
      title.textColor =
    	COLOR.text;
    
    
      header.addSpacer();
    
    
      const observation =
    	header.addText(
    	  formatDate(
    		date
    	  )
    	);
    
    
      observation.font =
    	Font.systemFont(8);
    
    
      observation.textColor =
    	COLOR.subText;
    
    
      widget.addSpacer(12);
    
    
      // TOP8
    
      for (
    	let i = 0;
    	i <
    	  Math.min(
    		8,
    		data.length
    	  );
    	i++
      ) {
    
    	addRankingRow(
    
    	  widget,
    
    	  data[i],
    
    	  i + 1,
    
    	  {
    		rankFont: 18,
    		flagFont: 15,
    		placeFont: 15,
    		countryFont: 8,
    		tempFont: 19,
    		diffFont: 9,
    		rankWidth: 25
    	  }
    
    	);
    
    
    	if (
    	  i < 7
    	) {
    
    	  widget.addSpacer(9);
    	}
      }
    
    
      widget.addSpacer();
    
    
      // データ元
    
      const source =
    	widget.addText(
    	  "気象庁|ClimatView"
    	);
    
    
      source.font =
    	Font.systemFont(8);
    
    
      source.textColor =
    	COLOR.subText;
    
    
      widget.url =
    	CONFIG.PAGE_URL;
    
    
      return widget;
    }
    
    
    // =====================================================
    // エラー
    // =====================================================
    
    function createErrorWidget(error) {
    
      const widget =
    	new ListWidget();
    
    
      setBackground(widget);
    
    
      widget.setPadding(
    	16,
    	16,
    	16,
    	16
      );
    
    
      const title =
    	widget.addText(
    	  "データを取得できません"
    	);
    
    
      title.font =
    	Font.boldSystemFont(14);
    
    
      title.textColor =
    	COLOR.text;
    
    
      widget.addSpacer(6);
    
    
      const message =
    	widget.addText(
    	  String(
    		error.message ||
    		error
    	  )
    	);
    
    
      message.font =
    	Font.systemFont(9);
    
    
      message.textColor =
    	COLOR.subText;
    
    
      return widget;
    }
    
    
    // =====================================================
    // MAIN
    // =====================================================
    
    let widget;
    
    
    try {
    
      // 最新日
    
      const latest =
    	await fetchLatestData();
    
    
      const currentCSV =
    	await decodeSJIS(
    	  latest.data
    	);
    
    
      let currentRows =
    	parseCSV(
    	  currentCSV
    	);
    
    
      if (
    	!currentRows.length
      ) {
    
    	throw new Error(
    	  "最新日のCSVを解析できませんでした"
    	);
      }
    
    
      // 前日
    
      const previousDate =
    	addDays(
    	  latest.date,
    	  -1
    	);
    
    
      const previousResult =
    	await fetchDailyData(
    	  previousDate
    	);
    
    
      let previousRows = [];
    
    
      if (
    	previousResult
      ) {
    
    	const previousCSV =
    	  await decodeSJIS(
    		previousResult.data
    	  );
    
    
    	previousRows =
    	  parseCSV(
    		previousCSV
    	  );
      }
    
    
      // 前日差
    
      currentRows =
    	attachPreviousDiff(
    	  currentRows,
    	  previousRows
    	);
    
    
      // ランキング
    
      const ranking =
    	makeRanking(
    	  currentRows
    	);
    
    
      if (
    	!ranking.length
      ) {
    
    	throw new Error(
    	  "ランキングデータがありません"
    	);
      }
    
    
      // Widget
    
      if (
    	config.widgetFamily ===
    	"small"
      ) {
    
    	widget =
    	  createSmall(
    		ranking,
    		latest.date
    	  );
    
      }
    
      else if (
    	config.widgetFamily ===
    	"large"
      ) {
    
    	widget =
    	  createLarge(
    		ranking,
    		latest.date
    	  );
    
      }
    
      else {
    
    	widget =
    	  createMedium(
    		ranking,
    		latest.date
    	  );
      }
    
    
      // 更新
    
      widget.refreshAfterDate =
    	new Date(
    	  Date.now() +
    	  CONFIG.REFRESH_MINUTES *
    	  60 *
    	  1000
    	);
    
    }
    
    catch (error) {
    
      console.error(
    	error
      );
    
    
      widget =
    	createErrorWidget(
    	  error
    	);
    }
    
    
    // =====================================================
    // プレビュー
    // =====================================================
    
    if (
      !config.runsInWidget
    ) {
    
      if (
    	config.widgetFamily ===
    	"small"
      ) {
    
    	await widget.presentSmall();
    
      }
    
      else if (
    	config.widgetFamily ===
    	"large"
      ) {
    
    	await widget.presentLarge();
    
      }
    
      else {
    
    	await widget.presentMedium();
      }
    }
    
    
    // =====================================================
    // 完了
    // =====================================================
    
    Script.setWidget(
      widget
    );
    
    Script.complete();
  3. Parameterに設定を入力する

    ウィジェットを長押しして「ウィジェットを編集」を開き、「Parameter」に 最高 または 最低 を入力します。空欄の場合は最高気温を表示します。切り替えるときは、この文字だけを書き換えてください。

  4. ホーム画面に追加して設定する

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

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

    「Parameter」に上で案内した値を直接入力します。 設定を変えるときは、この欄を書き換えてください。

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

最高気温・最低気温を切り替える

このウィジェットでは、Parameter(パラメーター)で最高気温・最低気温を切り替えます。

ウィジェットを長押しして、

「ウィジェットを編集」→「Parameter」に、表示したいランキングを入力してください。

今日の最高気温ランキングの場合は

パラメーター に 最高 と入力してください。

今日の最低気温ランキングの場合は

パラメーターに、 最低 と入力してください。

パラメーターに何も入力しなかった場合は、最高気温ランキングが表示されます。

前日差について

気温の右側に表示される小さな数字は、同じ観測地点の前日との差です。

+0.7° → 前日より0.7℃高い
-0.4° → 前日より0.4℃低い

「昨日の世界1位」と比較しているわけではなく、同じ観測地点同士を比較しています。

動作確認・使用結果

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

表示・使用時のポイント

気象庁ClimatViewの公開データから、対象日の最高・最低気温を比較します。最新の公開済み観測日が対象になるため、端末の今日の日付と一致しない場合があります。

更新履歴

1.0.0

  • 初回リリース

よくあるエラーと対処

確認すること
表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
今日の日付にならない
公開済みの観測日を表示する仕様です。
前日差が「—」になる
同じ地点の比較対象が取得できない場合の表示です。
他の世界ランキングと順位が違う
対象観測地点と集計日が異なる場合があります。

注意事項・制限

  • 取得先に収録された地点のランキングです。世界中の全観測所を網羅するものではありません。
  • 最高・最低の切り替えはParameterの設定で行います。
  • ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。

データ提供・参考資料

気象庁 ClimatView 日別世界の気象データ:公開されている地点の最高・最低気温。

SHARE

この記事をシェア