{"id":2780,"date":"2024-04-18T11:36:31","date_gmt":"2024-04-18T02:36:31","guid":{"rendered":"https:\/\/kdfm.jp\/?page_id=2780"},"modified":"2026-04-04T12:24:33","modified_gmt":"2026-04-04T03:24:33","slug":"2780-2","status":"publish","type":"page","link":"http:\/\/kdfm.jp\/?page_id=2780","title":{"rendered":"Web\u767e\u8449\u7bb1"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>\u795e\u4ed8\u3075\u308b\u3055\u3068\u6751 Web\u767e\u8449\u7bb1<\/title>\n    <script src=\"https:\/\/code.highcharts.com\/highcharts.js\"><\/script>\n    <script src=\"https:\/\/code.highcharts.com\/highcharts-more.js\"><\/script>\n    <style>\n        :root { --primary: #8bc34a; --bg: #f8f9fa; }\n        body { font-family: sans-serif; background: var(--bg); margin: 0; padding: 15px; }\n        .container { max-width: 1100px; margin: 0 auto; }\n        .dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 25px; }\n        .card { background: #fff; padding: 15px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); border-top: 4px solid var(--primary); text-align: center; }\n        .card-label { font-size: 11px; color: #777; margin-bottom: 5px; }\n        .card-value { font-size: 28px; font-weight: bold; }\n        .card-unit { font-size: 14px; color: #666; }\n        .section-title { border-left: 5px solid var(--primary); padding-left: 12px; margin: 30px 0 15px; font-size: 18px; }\n        .chart-grid { display: grid; grid-template-columns: 1fr; gap: 20px; }\n        @media (min-width: 768px) { .chart-grid { grid-template-columns: 1fr 1fr; } .span-full { grid-column: 1 \/ -1; } }\n        .chart-box { background: #fff; padding: 15px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); min-height: 350px; }\n        .tabs { display: flex; gap: 8px; margin-bottom: 15px; overflow-x: auto; }\n        .tab-btn { padding: 8px 16px; border: none; background: #e0e0e0; border-radius: 20px; cursor: pointer; }\n        .tab-btn.active { background: var(--primary); color: #fff; }\n        .tab-content { display: none; }\n        .tab-content.active { display: block; }\n    <\/style>\n<\/head>\n<body>\n<div class=\"container\">\n    <div style=\"text-align:center; margin-bottom: 20px;\">\n        <h1 style=\"margin:0; color: #558b2f; font-size: 24px;\">&#x1f469;&#x200d;&#x1f33e; \u795e\u4ed8\u3075\u308b\u3055\u3068\u6751 Web\u767e\u8449\u7bb1<\/h1>\n        <p id=\"last-update\" style=\"color:#999; font-size:12px;\">\u66f4\u65b0: &#8212;<\/p>\n    <\/div>\n\n    <div class=\"dashboard\">\n        <div class=\"card\"><div class=\"card-label\">\u6c17\u6e29<\/div><div class=\"card-value\"><span id=\"curr-temp\">&#8212;<\/span><span class=\"card-unit\">\u00b0C<\/span><\/div><\/div>\n        <div class=\"card\" style=\"border-top-color:#2196f3;\"><div class=\"card-label\">\u6e7f\u5ea6<\/div><div class=\"card-value\"><span id=\"curr-humi\">&#8212;<\/span><span class=\"card-unit\">%<\/span><\/div><\/div>\n        <div class=\"card\" style=\"border-top-color:#ff9800;\"><div class=\"card-label\">\u98a8\u901f<\/div><div class=\"card-value\"><span id=\"curr-wind\">&#8212;<\/span><span id=\"curr-wind-unit\" class=\"card-unit\">m\/s<\/span><\/div><\/div>\n        <div class=\"card\" style=\"border-top-color:#03a9f4;\"><div class=\"card-label\">\u672c\u65e5\u96e8\u91cf<\/div><div class=\"card-value\"><span id=\"curr-rain\">&#8212;<\/span><span class=\"card-unit\">mm<\/span><\/div><\/div>\n    <\/div>\n\n    <h2 class=\"section-title\">\u672c\u65e5\u306e\u8a73\u7d30\u30c7\u30fc\u30bf<\/h2>\n    <div class=\"chart-grid\">\n        <div id=\"day-temp\" class=\"chart-box span-full\"><\/div>\n        <div id=\"day-rain\" class=\"chart-box\"><\/div>\n        <div id=\"day-wind\" class=\"chart-box\"><\/div>\n    <\/div>\n\n    <div class=\"tabs\" style=\"margin-top:30px;\">\n        <button class=\"tab-btn active\" onclick=\"switchTab('week', event)\">\u9031\u9593<\/button>\n        <button class=\"tab-btn\" onclick=\"switchTab('month', event)\">\u6708\u9593<\/button>\n        <button class=\"tab-btn\" onclick=\"switchTab('year', event)\">\u5e74\u9593<\/button>\n    <\/div>\n    <div id=\"week\" class=\"tab-content active\"><div class=\"chart-grid\"><div id=\"week-temp\" class=\"chart-box\"><\/div><div id=\"week-rain\" class=\"chart-box\"><\/div><\/div><\/div>\n    <div id=\"month\" class=\"tab-content\"><div class=\"chart-grid\"><div id=\"month-temp\" class=\"chart-box\"><\/div><div id=\"month-rain\" class=\"chart-box\"><\/div><\/div><\/div>\n    <div id=\"year\" class=\"tab-content\"><div class=\"chart-grid\"><div id=\"year-temp\" class=\"chart-box\"><\/div><div id=\"year-rain\" class=\"chart-box\"><\/div><\/div><\/div>\n<\/div>\n\n<script>\nconst jsonPath = '\/weewx_kdfm\/belchertown\/json\/';\nconst DISPLAY_TZ = 'Asia\/Tokyo';\n\nHighcharts.setOptions({\n    time: { timezone: DISPLAY_TZ, useUTC: false },\n    lang: {\n        weekdays: ['\u65e5\u66dc\u65e5','\u6708\u66dc\u65e5','\u706b\u66dc\u65e5','\u6c34\u66dc\u65e5','\u6728\u66dc\u65e5','\u91d1\u66dc\u65e5','\u571f\u66dc\u65e5'],\n        shortWeekdays: ['\u65e5','\u6708','\u706b','\u6c34','\u6728','\u91d1','\u571f']\n    }\n});\n\nfunction decodeHtmlEntities(str) {\n    const textarea = document.createElement('textarea');\n    textarea.innerHTML = String(str ?? '');\n    return textarea.value;\n}\n\nfunction parseNumeric(val) {\n    if (val === undefined || val === null) return NaN;\n    const cleaned = decodeHtmlEntities(String(val)).replace(\/&nbsp;\/g, '').replace(\/[^0-9.-]\/g, '');\n    const num = parseFloat(cleaned);\n    return Number.isFinite(num) ? num : NaN;\n}\n\nfunction formatNumber(val, digits = 1) {\n    return Number.isFinite(val) ? val.toFixed(digits) : '--';\n}\n\nfunction normalizeTimestamp(ts) {\n    let n = Number(ts);\n    if (!Number.isFinite(n)) return ts;\n    if (Math.abs(n) < 1e12) n *= 1000;\n    return n;\n}\n\nfunction formatDateTime(ts) {\n    return new Intl.DateTimeFormat('ja-JP', {\n        timeZone: DISPLAY_TZ,\n        year: 'numeric', month: '2-digit', day: '2-digit',\n        hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false\n    }).format(new Date(normalizeTimestamp(ts)));\n}\n\nfunction getDisplayTimestamp(data) {\n    if (data?.generated_timestamp) return decodeHtmlEntities(data.generated_timestamp);\n    if (data?.current?.datetime) return decodeHtmlEntities(data.current.datetime);\n    if (data?.current?.datetime_raw) return formatDateTime(data.current.datetime_raw);\n    return '--';\n}\n\nfunction normalizeSeriesData(data) {\n    if (!Array.isArray(data)) return [];\n    return data\n        .filter(p => Array.isArray(p) && p.length >= 2)\n        .map(([t, v]) => [normalizeTimestamp(t), v]);\n}\n\nfunction positiveDiffSeries(data, maxDiff = Infinity) {\n    const src = normalizeSeriesData(data);\n    const out = [];\n    let prev = null;\n\n    for (const [t, raw] of src) {\n        const v = Number(raw);\n        if (!Number.isFinite(v)) {\n            prev = null;\n            continue;\n        }\n        if (prev === null) {\n            out.push([t, 0]);\n            prev = v;\n            continue;\n        }\n        let diff = v - prev;\n        if (!Number.isFinite(diff) || diff < 0 || diff > maxDiff) diff = 0;\n        out.push([t, diff]);\n        prev = v;\n    }\n    return out;\n}\n\nfunction sumSeries(data) {\n    return (data || []).reduce((sum, p) => {\n        const v = Number(p?.[1]);\n        return sum + (Number.isFinite(v) ? v : 0);\n    }, 0);\n}\n\nfunction jstDayKey(ts) {\n    return new Intl.DateTimeFormat('en-CA', {\n        timeZone: DISPLAY_TZ,\n        year: 'numeric', month: '2-digit', day: '2-digit'\n    }).format(new Date(normalizeTimestamp(ts)));\n}\n\nfunction dayKeyToTimestamp(dayKey) {\n    return new Date(`${dayKey}T00:00:00+09:00`).getTime();\n}\n\nfunction aggregateDaily(data) {\n    const bucket = new Map();\n    for (const [t, raw] of normalizeSeriesData(data)) {\n        const v = Number(raw);\n        if (!Number.isFinite(v)) continue;\n        const key = jstDayKey(t);\n        bucket.set(key, (bucket.get(key) || 0) + v);\n    }\n    return Array.from(bucket.entries())\n        .sort((a, b) => a[0].localeCompare(b[0]))\n        .map(([key, total]) => [dayKeyToTimestamp(key), Number(total.toFixed(2))]);\n}\n\nfunction isRainSeries(key, seriesInfo) {\n    const k = (key || '').toLowerCase();\n    const n = ((seriesInfo && seriesInfo.name) || '').toLowerCase();\n    return k.includes('rain') || n.includes('rain');\n}\n\nfunction isRainRateSeries(key, seriesInfo) {\n    const k = (key || '').toLowerCase();\n    const n = ((seriesInfo && seriesInfo.name) || '').toLowerCase();\n    return k.includes('rainrate') || n.includes('rain rate');\n}\n\nfunction isRainTotalSeries(key, seriesInfo) {\n    const k = (key || '').toLowerCase();\n    const n = ((seriesInfo && seriesInfo.name) || '').toLowerCase();\n    return k.includes('raintotal') || k.includes('totalrain') || n.includes('rain total') || n.includes('total rain');\n}\n\nfunction buildDisplayedRainData(chartObj, periodId) {\n    const seriesMap = chartObj?.series || {};\n    const entries = Object.entries(seriesMap);\n    const rainRateEntry = entries.find(([key, info]) => isRainRateSeries(key, info));\n\n    if (rainRateEntry) {\n        const raw = rainRateEntry[1]?.data || [];\n        const diff = positiveDiffSeries(raw, periodId === 'day' ? 50 : 100);\n        return periodId === 'day' ? diff : aggregateDaily(diff);\n    }\n\n    const rainTotalEntry = entries.find(([key, info]) => isRainTotalSeries(key, info));\n    if (rainTotalEntry) {\n        const raw = rainTotalEntry[1]?.data || [];\n        const diff = positiveDiffSeries(raw, periodId === 'day' ? 50 : 100);\n        return periodId === 'day' ? diff : aggregateDaily(diff);\n    }\n\n    return [];\n}\n\nfunction buildRainChartSeries(chartObj, periodId) {\n    const seriesMap = chartObj?.series || {};\n    const displayed = buildDisplayedRainData(chartObj, periodId);\n    const series = [{\n        name: '\u96e8\u91cf',\n        data: displayed,\n        type: 'column',\n        color: '#2196f3',\n        pointPadding: 0.05,\n        groupPadding: 0.08,\n        pointRange: periodId === 'day' ? undefined : 24 * 3600 * 1000 * 0.8,\n        tooltip: { valueSuffix: ' mm' },\n        visible: true,\n        showInLegend: true\n    }];\n\n    for (const [key, info] of Object.entries(seriesMap)) {\n        const rawData = normalizeSeriesData(info?.data || []);\n        if (isRainRateSeries(key, info)) {\n            series.push({\n                name: info.name || key,\n                data: rawData,\n                type: 'line',\n                color: info.color || '#90caf9',\n                tooltip: { valueSuffix: ' (raw)' },\n                visible: false,\n                showInLegend: true\n            });\n        } else if (isRainTotalSeries(key, info)) {\n            series.push({\n                name: info.name || key,\n                data: rawData,\n                type: 'line',\n                color: info.color || '#666666',\n                tooltip: { valueSuffix: ' (raw)' },\n                visible: false,\n                showInLegend: true\n            });\n        }\n    }\n    return series;\n}\n\nfunction formatAxisTime(value, periodId) {\n    const opts = { timeZone: DISPLAY_TZ, hour12: false };\n    if (periodId === 'day') {\n        opts.hour = '2-digit';\n        opts.minute = '2-digit';\n    } else {\n        opts.month = 'numeric';\n        opts.day = 'numeric';\n    }\n    return new Intl.DateTimeFormat('ja-JP', opts).format(new Date(value));\n}\n\nfunction isWindDirectionSeries(key, info) {\n    const text = `${key} ${info?.name || ''} ${info?.obsType || ''}`.toLowerCase();\n    return text.includes('winddir') || text.includes('direction') || text.includes('dir');\n}\n\nfunction isWindSpeedSeries(key, info) {\n    const text = `${key} ${info?.name || ''} ${info?.obsType || ''}`.toLowerCase();\n    return text.includes('wind') && !isWindDirectionSeries(key, info);\n}\n\nfunction buildStandardSeries(chartObj, unit, title) {\n    const isWindChart = title.includes('\u98a8\u901f\u30fb\u98a8\u5411');\n    const seriesMap = chartObj?.series || {};\n    return Object.keys(seriesMap).map(key => {\n        const info = seriesMap[key] || {};\n        const normalized = normalizeSeriesData(info.data || []);\n        const convertedData = isWindChart && isWindSpeedSeries(key, info)\n            ? normalized.map(([x, y]) => [x, Number.isFinite(y) ? y \/ 3.6 : y])\n            : normalized;\n        const suffix = isWindChart && isWindSpeedSeries(key, info)\n            ? ' m\/s'\n            : (isWindChart && isWindDirectionSeries(key, info) ? ' \u00b0' : (unit ? ` ${unit}` : ''));\n        return {\n            name: info.name || key,\n            data: convertedData,\n            type: (info.lineWidth === 0 || info.marker?.enabled === 'true') ? 'scatter' : (chartObj.options?.type || 'spline'),\n            color: info.color,\n            yAxis: Number.isFinite(Number(info.yAxis)) ? Number(info.yAxis) : 0,\n            tooltip: { valueSuffix: suffix },\n            visible: !isRainSeries(key, info)\n        };\n    });\n}\n\nfunction getYAxes(chartObj, unit, title) {\n    if (title.includes('\u98a8\u901f\u30fb\u98a8\u5411')) {\n        return [\n            { title: { text: 'Wind Speed (m\/s)' }, min: 0 },\n            { title: { text: 'Wind Direction (\u00b0)' }, min: 0, max: 360, opposite: true }\n        ];\n    }\n    return [{\n        title: { text: chartObj?.options?.yAxis_label || (unit === '\u00b0C' ? 'Temperature (\u00b0C)' : unit === 'mm' ? 'Rainfall (mm)' : 'Value') },\n        min: unit === 'mm' ? 0 : null\n    }];\n}\n\nfunction renderChart(target, title, chartObj, unit, periodId) {\n    if (!chartObj || !chartObj.series) return;\n\n    const hasRain = Object.keys(chartObj.series).some(key => isRainSeries(key, chartObj.series[key] || {}));\n    const seriesArr = (unit === 'mm' && hasRain)\n        ? buildRainChartSeries(chartObj, periodId)\n        : buildStandardSeries(chartObj, unit, title);\n\n    Highcharts.chart(target, {\n        chart: { zoomType: 'x', backgroundColor: 'transparent' },\n        title: { text: title, style: { fontSize: '15px' } },\n        xAxis: {\n            type: 'datetime',\n            labels: {\n                formatter: function () {\n                    return formatAxisTime(this.value, periodId);\n                }\n            }\n        },\n        yAxis: getYAxes(chartObj, unit, title),\n        tooltip: {\n            shared: true,\n            formatter: function () {\n                const header = `<b>${formatDateTime(this.x)}<\/b>`;\n                const rows = (this.points || []).map(p => `<br\/>${p.series.name}: <b>${Highcharts.numberFormat(p.y, 1)}<\/b>${p.series.tooltipOptions.valueSuffix || ''}`);\n                return header + rows.join('');\n            }\n        },\n        legend: { enabled: true },\n        series: seriesArr,\n        credits: { enabled: false }\n    });\n}\n\nfunction detectWindUnit(rawText, data) {\n    const label = decodeHtmlEntities(data?.unit_label?.windSpeed || data?.unit_label?.wind || '');\n    const raw = decodeHtmlEntities(String(rawText || ''));\n    const source = `${label} ${raw}`.toLowerCase();\n    if (source.includes('km\/h') || source.includes('kph')) return 'km\/h';\n    if (source.includes('m\/s')) return 'm\/s';\n    if (source.includes('mph')) return 'mph';\n    if (source.includes('kt') || source.includes('knot')) return 'kt';\n    return 'km\/h';\n}\n\nfunction convertWindToMs(value, unit) {\n    if (!Number.isFinite(value)) return NaN;\n    switch ((unit || '').toLowerCase()) {\n        case 'km\/h': return value \/ 3.6;\n        case 'mph': return value * 0.44704;\n        case 'kt': return value * 0.514444;\n        case 'm\/s': return value;\n        default: return value \/ 3.6;\n    }\n}\n\nfunction getCurrentWindMs(data, dayJson) {\n    const curr = data?.current || {};\n    const rawWind = curr.windspeed ?? curr.windSpeed ?? null;\n    let windVal = parseNumeric(rawWind);\n    let windUnit = detectWindUnit(rawWind, data);\n\n    if (!Number.isFinite(windVal) && dayJson?.chart2?.series?.windSpeed?.data?.length) {\n        const series = normalizeSeriesData(dayJson.chart2.series.windSpeed.data);\n        const latest = series[series.length - 1]?.[1];\n        windVal = Number(latest);\n        windUnit = detectWindUnit(dayJson?.chart2?.series?.windSpeed?.name || '', data);\n    }\n\n    const ms = convertWindToMs(windVal, windUnit);\n    return Number.isFinite(ms) ? ms : NaN;\n}\n\nfunction getTodayRainTotal(dayJson) {\n    const displayed = buildDisplayedRainData(dayJson?.chart3, 'day');\n    const total = sumSeries(displayed);\n    return Number.isFinite(total) ? total : NaN;\n}\n\nfunction switchTab(id, ev) {\n    document.querySelectorAll('.tab-content, .tab-btn').forEach(el => el.classList.remove('active'));\n    document.getElementById(id).classList.add('active');\n    if (ev?.currentTarget) ev.currentTarget.classList.add('active');\n    window.dispatchEvent(new Event('resize'));\n}\n\nasync function loadDashboard() {\n    try {\n        const [dashboardRes, dayRes] = await Promise.all([\n            fetch(`${jsonPath}weewx_data.json`),\n            fetch(`${jsonPath}day.json`)\n        ]);\n        const data = await dashboardRes.json();\n        const dayJson = await dayRes.json();\n        const curr = data.current || {};\n\n        document.getElementById('curr-temp').textContent = formatNumber(parseNumeric(curr.outTemp), 1);\n        document.getElementById('curr-humi').textContent = formatNumber(parseNumeric(curr.outHumidity), 0);\n\n        const windMs = getCurrentWindMs(data, dayJson);\n        document.getElementById('curr-wind').textContent = formatNumber(windMs, 1);\n        document.getElementById('curr-wind-unit').textContent = 'm\/s';\n\n        const todayRain = getTodayRainTotal(dayJson);\n        document.getElementById('curr-rain').textContent = formatNumber(todayRain, 1);\n\n        document.getElementById('last-update').textContent = '\u66f4\u65b0: ' + getDisplayTimestamp(data);\n    } catch (e) {\n        console.error('Dashboard error:', e);\n    }\n}\n\nasync function loadCharts() {\n    const periods = [\n        { id: 'day', file: 'day.json', title: '\u5f53\u65e5' },\n        { id: 'week', file: 'week.json', title: '\u9031\u9593' },\n        { id: 'month', file: 'month.json', title: '\u6708\u9593' },\n        { id: 'year', file: 'year.json', title: '\u5e74\u9593' }\n    ];\n\n    for (const p of periods) {\n        try {\n            const res = await fetch(`${jsonPath}${p.file}`);\n            const data = await res.json();\n\n            renderChart(`${p.id}-temp`, `${p.title} \u6c17\u6e29\u63a8\u79fb`, data.chart1, '\u00b0C', p.id);\n\n            if (p.id === 'day') {\n                renderChart('day-wind', '\u5f53\u65e5 \u98a8\u901f\u30fb\u98a8\u5411', data.chart2, 'm\/s', p.id);\n                renderChart('day-rain', '\u5f53\u65e5 \u96e8\u91cf', data.chart3, 'mm', p.id);\n            } else {\n                renderChart(`${p.id}-rain`, `${p.title} \u96e8\u91cf\u63a8\u79fb`, data.chart3, 'mm', p.id);\n            }\n        } catch (e) {\n            console.error(`${p.file} load error:`, e);\n        }\n    }\n}\n\nwindow.onload = () => {\n    loadDashboard();\n    loadCharts();\n};\n<\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>\u795e\u4ed8\u3075\u308b\u3055\u3068\u6751 Web\u767e\u8449\u7bb1 &#x1f469;&#x200d;&#038;#x1f33e &#8230; <\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":[],"_links":{"self":[{"href":"http:\/\/kdfm.jp\/index.php?rest_route=\/wp\/v2\/pages\/2780"}],"collection":[{"href":"http:\/\/kdfm.jp\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"http:\/\/kdfm.jp\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"http:\/\/kdfm.jp\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/kdfm.jp\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2780"}],"version-history":[{"count":14,"href":"http:\/\/kdfm.jp\/index.php?rest_route=\/wp\/v2\/pages\/2780\/revisions"}],"predecessor-version":[{"id":2995,"href":"http:\/\/kdfm.jp\/index.php?rest_route=\/wp\/v2\/pages\/2780\/revisions\/2995"}],"wp:attachment":[{"href":"http:\/\/kdfm.jp\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2780"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}