
太陽で発生しているフレアの活動状況を、iPhoneのホーム画面から確認できるScriptableウィジェットです。
現在の太陽活動レベルや今日最大のフレア、C・M・Xクラスの発生回数に加えて、過去24時間のX線強度をグラフで表示します。
小・中・大の3サイズに対応しているので、ホーム画面の空きスペースに合わせて利用できます。
ウィジェットの表示内容
NOAAのGOES X線データとフレアの記録を取得します。
サイズごとの表示
- 小:現在の活動・当日の最大フレアと、X線の変化を表示します。
- 中:C・M・Xクラスの発生件数なども確認できます。
- 大:中サイズの情報を共通に、グラフを広く表示します。
設定手順
上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。
- Scriptableを用意する
App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。
- コードをコピーして保存する
下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、今日の太陽フレアなどの名前で保存してください。
保存したらこのページに戻り、次の手順へ進みます。
// Variables used by Scriptable. // These must be at the very top of the file. Do not edit. // icon-color: orange; icon-glyph: sun; // Version: 1.0.0 // ===================================================== // 今日の太陽フレア - Scriptable Widget // NOAA / GOES X-ray データ使用 // // 小・中・大サイズ対応 // APIキー不要 // 全サイズ:過去24時間X線グラフ表示 // ===================================================== // ===================================================== // 設定 // ===================================================== const FLARE_URL = "https://services.swpc.noaa.gov/json/goes/primary/xray-flares-7-day.json"; const XRAY_URL = "https://services.swpc.noaa.gov/json/goes/primary/xrays-1-day.json"; const CACHE_NAME = "solar_flare_widget_cache.json"; // ===================================================== // 色 // ===================================================== const COLORS = { background: new Color("#090B10"), text: Color.white(), subText: new Color("#9CA3AF"), quiet: new Color("#4ADE80"), c: new Color("#FACC15"), m: new Color("#FB923C"), x: new Color("#EF4444"), grid: new Color("#374151"), graph: new Color("#FDBA74") }; // ===================================================== // データ取得 // ===================================================== async function fetchJSON(url) { const req = new Request(url); req.timeoutInterval = 10; return await req.loadJSON(); } // ===================================================== // キャッシュ // ===================================================== function getCacheFile() { const fm = FileManager.local(); const dir = fm.joinPath( fm.documentsDirectory(), "SolarFlareWidget" ); if (!fm.fileExists(dir)) { fm.createDirectory(dir); } return { fm, path: fm.joinPath(dir, CACHE_NAME) }; } function saveCache(data) { const cache = getCacheFile(); try { cache.fm.writeString( cache.path, JSON.stringify(data) ); } catch (e) {} } function loadCache() { const cache = getCacheFile(); try { if (!cache.fm.fileExists(cache.path)) { return null; } return JSON.parse( cache.fm.readString(cache.path) ); } catch (e) { return null; } } // ===================================================== // NOAAデータ取得 // ===================================================== async function loadData() { try { const flareData = await fetchJSON(FLARE_URL); const xrayData = await fetchJSON(XRAY_URL); const data = { flares: flareData, xrays: xrayData, updated: Date.now() }; saveCache(data); return data; } catch (e) { const cache = loadCache(); if (cache) { return cache; } throw e; } } // ===================================================== // 日本時間 // ===================================================== function japanDateString(date) { return new Intl.DateTimeFormat( "ja-JP", { timeZone: "Asia/Tokyo", year: "numeric", month: "2-digit", day: "2-digit" } ).format(date); } function isToday(date) { return ( japanDateString(date) === japanDateString(new Date()) ); } function formatTime(date) { if (!date) { return "--:--"; } return new Intl.DateTimeFormat( "ja-JP", { timeZone: "Asia/Tokyo", hour: "2-digit", minute: "2-digit", hour12: false } ).format(date); } // ===================================================== // フレアクラス解析 // ===================================================== function parseFlareClass(value) { if (!value) { return { letter: "-", number: 0 }; } const match = String(value).match(/([ABCMX])\s*([0-9.]+)/i); if (!match) { return { letter: "-", number: 0 }; } return { letter: match[1].toUpperCase(), number: parseFloat(match[2]) }; } function flareScore(value) { const f = parseFlareClass(value); const rank = { A: 1, B: 2, C: 3, M: 4, X: 5 }; return ( (rank[f.letter] || 0) * 1000 + f.number ); } function getFlareTime(flare) { const value = flare.max_time || flare.peak_time || flare.begin_time || flare.time_tag || flare.end_time; if (!value) return null; const d = new Date(value); if (isNaN(d.getTime())) { return null; } return d; } function getFlareClass(flare) { return ( flare.max_class || flare.current_class || flare.flare_class || flare.class || "" ); } // ===================================================== // 今日のフレア解析 // ===================================================== function analyzeFlares(flares) { const today = []; for (const flare of flares) { const time = getFlareTime(flare); if (!time) continue; if (isToday(time)) { today.push({ raw: flare, time, className: getFlareClass(flare) }); } } today.sort( (a, b) => a.time.getTime() - b.time.getTime() ); let maxFlare = null; for (const flare of today) { if ( !maxFlare || flareScore(flare.className) > flareScore(maxFlare.className) ) { maxFlare = flare; } } const counts = { C: 0, M: 0, X: 0 }; for (const flare of today) { const c = parseFlareClass(flare.className); if (counts[c.letter] !== undefined) { counts[c.letter]++; } } return { today, maxFlare, latest: today.length ? today[today.length - 1] : null, counts }; } // ===================================================== // X線データ整理 // ===================================================== function normalizeXrayData(data) { const result = []; for (const item of data) { if ( item.energy && !String(item.energy).includes("0.1-0.8") ) { continue; } const time = new Date( item.time_tag || item.time || item.timestamp ); const flux = Number( item.flux ?? item.value ?? item.xray_flux ); if ( !isNaN(time.getTime()) && isFinite(flux) && flux > 0 ) { result.push({ time, flux }); } } result.sort( (a, b) => a.time.getTime() - b.time.getTime() ); return result; } // ===================================================== // X線強度 → クラス // ===================================================== function fluxToClass(flux) { if (!flux || flux <= 0) { return "-"; } let letter; let base; if (flux >= 1e-4) { letter = "X"; base = 1e-4; } else if (flux >= 1e-5) { letter = "M"; base = 1e-5; } else if (flux >= 1e-6) { letter = "C"; base = 1e-6; } else if (flux >= 1e-7) { letter = "B"; base = 1e-7; } else { letter = "A"; base = 1e-8; } return ( letter + (flux / base).toFixed(1) ); } // ===================================================== // 活動レベル // ===================================================== function activityInfo(flux) { if (flux >= 1e-4) { return { label: "非常に活発", color: COLORS.x }; } if (flux >= 1e-5) { return { label: "活発", color: COLORS.m }; } if (flux >= 1e-6) { return { label: "やや活発", color: COLORS.c }; } return { label: "低", color: COLORS.quiet }; } // ===================================================== // テキスト // ===================================================== function addText( stack, text, size, color = COLORS.text, weight = "regular" ) { const t = stack.addText(String(text)); t.font = weight === "bold" ? Font.boldSystemFont(size) : Font.systemFont(size); t.textColor = color; t.lineLimit = 1; return t; } // ===================================================== // ヘッダー // ===================================================== function addHeader(widget, size = 13) { const header = widget.addStack(); header.centerAlignContent(); addText( header, "☀️", size + 2 ); header.addSpacer(5); addText( header, "今日の太陽フレア", size, COLORS.text, "bold" ); } // ===================================================== // グラフ // ===================================================== function createGraph( data, width, height, compact = false ) { const ctx = new DrawContext(); ctx.size = new Size(width, height); ctx.opaque = false; ctx.respectScreenScale = true; const left = compact ? 12 : 42; const right = 8; const top = 6; const bottom = compact ? 5 : 22; const graphW = width - left - right; const graphH = height - top - bottom; // A ~ X以上 const minLog = -8; const maxLog = -3; // --------------------------------- // C / M / X 境界線 // --------------------------------- const levels = [ { value: 1e-6, label: "C" }, { value: 1e-5, label: "M" }, { value: 1e-4, label: "X" } ]; for (const level of levels) { const log = Math.log10(level.value); const y = top + (maxLog - log) / (maxLog - minLog) * graphH; const line = new Path(); line.move( new Point( left, y ) ); line.addLine( new Point( width - right, y ) ); ctx.addPath(line); ctx.setStrokeColor( COLORS.grid ); ctx.setLineWidth(1); ctx.strokePath(); // 小サイズは文字を省略 if (!compact) { ctx.setFont( Font.systemFont(15) ); ctx.setTextColor( COLORS.subText ); ctx.drawTextInRect( level.label, new Rect( 5, y - 9, 28, 18 ) ); } } // --------------------------------- // データ // --------------------------------- if (data.length >= 2) { const now = data[data.length - 1] .time .getTime(); const start = now - 24 * 60 * 60 * 1000; const points = data.filter( d => d.time.getTime() >= start ); if (points.length >= 2) { const graphPath = new Path(); let started = false; for (const p of points) { const ratio = ( p.time.getTime() - start ) / ( now - start ); const x = left + ratio * graphW; let log = Math.log10(p.flux); log = Math.max( minLog, Math.min( maxLog, log ) ); const y = top + ( maxLog - log ) / ( maxLog - minLog ) * graphH; if (!started) { graphPath.move( new Point( x, y ) ); started = true; } else { graphPath.addLine( new Point( x, y ) ); } } ctx.addPath( graphPath ); ctx.setStrokeColor( COLORS.graph ); ctx.setLineWidth( compact ? 2.5 : 3 ); ctx.strokePath(); } } // --------------------------------- // 時間表示 // --------------------------------- if (!compact) { ctx.setFont( Font.systemFont(14) ); ctx.setTextColor( COLORS.subText ); ctx.drawTextInRect( "24時間前", new Rect( left, height - 18, 100, 18 ) ); ctx.drawTextInRect( "現在", new Rect( width - 50, height - 18, 45, 18 ) ); } return ctx.getImage(); } // ===================================================== // 小サイズ // ===================================================== function buildSmall( widget, analysis, xrayData, latestFlux ) { addHeader( widget, 12 ); widget.addSpacer(8); const info = widget.addStack(); info.centerAlignContent(); const activity = activityInfo(latestFlux); const left = info.addStack(); left.layoutVertically(); addText( left, "現在", 9, COLORS.subText ); left.addSpacer(1); addText( left, activity.label, 21, activity.color, "bold" ); info.addSpacer(); const right = info.addStack(); right.layoutVertically(); addText( right, "今日最大", 8, COLORS.subText ); right.addSpacer(2); const max = analysis.maxFlare ? analysis.maxFlare.className : "なし"; addText( right, max, 14, COLORS.text, "bold" ); widget.addSpacer(7); addText( widget, "過去24時間", 8, COLORS.subText ); widget.addSpacer(2); const graph = widget.addImage( createGraph( xrayData, 300, 82, true ) ); graph.resizable = true; } // ===================================================== // 中サイズ // ===================================================== function buildMedium( widget, analysis, xrayData, latestFlux ) { addHeader(widget); widget.addSpacer(8); const body = widget.addStack(); // 左 const left = body.addStack(); left.layoutVertically(); const activity = activityInfo(latestFlux); addText( left, "現在の活動", 9, COLORS.subText ); left.addSpacer(1); addText( left, activity.label, 22, activity.color, "bold" ); left.addSpacer(3); addText( left, "現在 " + fluxToClass(latestFlux), 9, COLORS.text ); body.addSpacer(); // 右 const right = body.addStack(); right.layoutVertically(); addText( right, "今日最大", 9, COLORS.subText ); right.addSpacer(1); const max = analysis.maxFlare ? analysis.maxFlare.className : "なし"; addText( right, max, 19, COLORS.text, "bold" ); right.addSpacer(4); addText( right, "C " + analysis.counts.C + " M " + analysis.counts.M + " X " + analysis.counts.X, 8, COLORS.subText ); widget.addSpacer(6); const graph = widget.addImage( createGraph( xrayData, 600, 115, false ) ); graph.resizable = true; } // ===================================================== // 大サイズ // ===================================================== function buildLarge( widget, analysis, xrayData, latestFlux ) { addHeader(widget); widget.addSpacer(12); const body = widget.addStack(); // 左 const left = body.addStack(); left.layoutVertically(); const activity = activityInfo(latestFlux); addText( left, "現在の活動", 10, COLORS.subText ); left.addSpacer(2); addText( left, activity.label, 27, activity.color, "bold" ); left.addSpacer(6); addText( left, "現在 " + fluxToClass(latestFlux), 11, COLORS.text ); body.addSpacer(); // 右 const right = body.addStack(); right.layoutVertically(); addText( right, "今日最大", 10, COLORS.subText ); right.addSpacer(2); const max = analysis.maxFlare ? analysis.maxFlare.className : "なし"; addText( right, max, 25, COLORS.text, "bold" ); right.addSpacer(6); addText( right, "C " + analysis.counts.C + "回 M " + analysis.counts.M + "回 X " + analysis.counts.X + "回", 10, COLORS.subText ); widget.addSpacer(16); addText( widget, "過去24時間のX線強度", 11, COLORS.text, "bold" ); widget.addSpacer(6); const graph = widget.addImage( createGraph( xrayData, 600, 210, false ) ); graph.resizable = true; widget.addSpacer(); const latest = analysis.latest ? formatTime( analysis.latest.time ) : "--:--"; addText( widget, "最終フレア " + latest + " | NOAA / GOES", 9, COLORS.subText ); } // ===================================================== // メイン // ===================================================== async function main() { const widget = new ListWidget(); widget.backgroundColor = COLORS.background; widget.setPadding( 14, 14, 14, 14 ); try { const data = await loadData(); const analysis = analyzeFlares( data.flares ); const xrayData = normalizeXrayData( data.xrays ); const latestFlux = xrayData.length ? xrayData[ xrayData.length - 1 ].flux : 0; const family = config.widgetFamily || "medium"; if (family === "small") { buildSmall( widget, analysis, xrayData, latestFlux ); } else if ( family === "large" ) { buildLarge( widget, analysis, xrayData, latestFlux ); } else { buildMedium( widget, analysis, xrayData, latestFlux ); } widget.refreshAfterDate = new Date( Date.now() + 15 * 60 * 1000 ); } catch (e) { addText( widget, "☀️ 今日の太陽フレア", 14, COLORS.text, "bold" ); widget.addSpacer(); addText( widget, "データを取得できませんでした", 11, COLORS.subText ); widget.addSpacer(4); addText( widget, String(e), 8, COLORS.subText ); } if (!config.runsInWidget) { await widget.presentMedium(); } Script.setWidget(widget); Script.complete(); } await main(); - ホーム画面に追加して設定する
iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。
追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。
「Parameter」は空欄で構いません。
編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。
C・M・Xって何?
C・M・Xは、太陽フレアの強さを表す分類です。
太陽フレアは弱い順に、
A → B → C → M → X
と分類されます。
Cクラスは比較的小規模なフレア、Mクラスは中〜大規模、Xクラスは非常に強力なフレアです。
さらに「M2.0」「X1.5」のように数字で細かく強さが表されます。同じクラスなら数字が大きいほど強いフレアです。
ウィジェットに表示される
C 3回 M 1回 X 0回
などの数字は、その日にそれぞれのクラスの太陽フレアが何回観測されたかを表しています。
動作確認・使用結果
| 確認した端末 | iPhone 17 ProMAX |
|---|---|
| iOS | iOS27 |
| Scriptable | 1.7.19 |
| 確認したサイズ | 小・中・大 |
| バージョン | 1.0.0 |
| 動作確認日 | 2026-09-24 |
表示・使用時のポイント
NOAAのGOES X線データとフレアの記録を取得します。現在のX線強度と記録されたフレアは別の指標として表示します。
更新履歴
1.0.0
- 初回リリース
よくあるエラーと対処
- 確認すること
- 表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
- グラフが表示されない
- 取得した時間帯のデータが不足している場合があります。通信を確認して再実行してください。
- 発生件数が0になる
- 対象期間に該当するクラスの記録がない場合は0になります。
- 値が古い
- 観測時刻と通信状態を確認してください。
注意事項・制限
- 太陽の活動を眺めるための表示です。通信障害などの発生を個別に予測するものではありません。
- ホーム画面は常時監視ではありません。最新の観測時刻を確認してください。
- ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。
データ提供・参考資料
NOAA GOES X-ray Flux:X線観測値。
NOAA GOES フレア記録:フレアの発生データ。
SHARE

