Coverage for flask/src/rasp_shutter/metrics/webapi/page.py: 12%

185 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-08-23 19:38 +0900

1#!/usr/bin/env python3 

2""" 

3シャッターメトリクス表示ページ 

4 

5シャッター操作の統計情報とグラフを表示するWebページを提供します。 

6""" 

7 

8from __future__ import annotations 

9 

10import datetime 

11import io 

12import json 

13import logging 

14 

15import my_lib.webapp.config 

16import rasp_shutter.metrics.collector 

17from PIL import Image, ImageDraw 

18 

19import flask 

20 

21blueprint = flask.Blueprint("metrics", __name__, url_prefix=my_lib.webapp.config.URL_PREFIX) 

22 

23 

24@blueprint.route("/api/metrics", methods=["GET"]) 

25def metrics_view(): 

26 """メトリクスダッシュボードページを表示""" 

27 try: 

28 # 設定からメトリクスデータパスを取得 

29 config = flask.current_app.config["CONFIG"] 

30 metrics_data_path = config.get("metrics", {}).get("data") 

31 

32 # データベースファイルの存在確認 

33 if not metrics_data_path: 

34 return flask.Response( 

35 "<html><body><h1>メトリクス設定が見つかりません</h1>" 

36 "<p>config.yamlでmetricsセクションが設定されていません。</p></body></html>", 

37 mimetype="text/html", 

38 status=503, 

39 ) 

40 

41 from pathlib import Path 

42 

43 db_path = Path(metrics_data_path) 

44 if not db_path.exists(): 

45 return flask.Response( 

46 f"<html><body><h1>メトリクスデータベースが見つかりません</h1>" 

47 f"<p>データベースファイル: {db_path}</p>" 

48 f"<p>システムが十分に動作してからメトリクスが生成されます。</p></body></html>", 

49 mimetype="text/html", 

50 status=503, 

51 ) 

52 

53 # メトリクス収集器を取得 

54 collector = rasp_shutter.metrics.collector.get_collector(metrics_data_path) 

55 

56 # 全期間のデータを取得 

57 operation_metrics = collector.get_all_operation_metrics() 

58 failure_metrics = collector.get_all_failure_metrics() 

59 

60 # 統計データを生成 

61 stats = generate_statistics(operation_metrics, failure_metrics) 

62 

63 # データ期間を計算 

64 data_period = calculate_data_period(operation_metrics) 

65 

66 # HTMLを生成 

67 html_content = generate_metrics_html(stats, operation_metrics, data_period) 

68 

69 return flask.Response(html_content, mimetype="text/html") 

70 

71 except Exception as e: 

72 logging.exception("メトリクス表示の生成エラー") 

73 return flask.Response(f"エラー: {e!s}", mimetype="text/plain", status=500) 

74 

75 

76@blueprint.route("/favicon.ico", methods=["GET"]) 

77def favicon(): 

78 """動的生成されたシャッターメトリクス用favicon.icoを返す""" 

79 try: 

80 # シャッターメトリクスアイコンを生成 

81 img = generate_shutter_metrics_icon() 

82 

83 # ICO形式で出力 

84 output = io.BytesIO() 

85 img.save(output, format="ICO", sizes=[(32, 32)]) 

86 output.seek(0) 

87 

88 return flask.Response( 

89 output.getvalue(), 

90 mimetype="image/x-icon", 

91 headers={ 

92 "Cache-Control": "public, max-age=3600", # 1時間キャッシュ 

93 "Content-Type": "image/x-icon", 

94 }, 

95 ) 

96 except Exception: 

97 logging.exception("favicon生成エラー") 

98 return flask.Response("", status=500) 

99 

100 

101def generate_shutter_metrics_icon(): 

102 """シャッターメトリクス用のアイコンを動的生成(アンチエイリアス対応)""" 

103 # アンチエイリアスのため4倍サイズで描画してから縮小 

104 scale = 4 

105 size = 32 

106 large_size = size * scale 

107 

108 # 大きなサイズで描画 

109 img = Image.new("RGBA", (large_size, large_size), (0, 0, 0, 0)) 

110 draw = ImageDraw.Draw(img) 

111 

112 # 背景円(メトリクスらしい青色) 

113 margin = 2 * scale 

114 draw.ellipse( 

115 [margin, margin, large_size - margin, large_size - margin], 

116 fill=(52, 152, 219, 255), 

117 outline=(41, 128, 185, 255), 

118 width=2 * scale, 

119 ) 

120 

121 # グラフっぽい線を描画(座標を4倍に拡大) 

122 points = [ 

123 (8 * scale, 20 * scale), 

124 (12 * scale, 16 * scale), 

125 (16 * scale, 12 * scale), 

126 (20 * scale, 14 * scale), 

127 (24 * scale, 10 * scale), 

128 ] 

129 

130 # 折れ線グラフ 

131 for i in range(len(points) - 1): 

132 draw.line([points[i], points[i + 1]], fill=(255, 255, 255, 255), width=2 * scale) 

133 

134 # データポイント 

135 point_size = 1 * scale 

136 for point in points: 

137 draw.ellipse( 

138 [point[0] - point_size, point[1] - point_size, point[0] + point_size, point[1] + point_size], 

139 fill=(255, 255, 255, 255), 

140 ) 

141 

142 # 32x32に縮小してアンチエイリアス効果を得る 

143 return img.resize((size, size), Image.LANCZOS) 

144 

145 

146def calculate_data_period(operation_metrics: list[dict]) -> dict: 

147 """データ期間を計算""" 

148 if not operation_metrics: 

149 return {"total_days": 0, "start_date": None, "end_date": None, "display_text": "データなし"} 

150 

151 # 日付のみを抽出 

152 dates = [op.get("date") for op in operation_metrics if op.get("date")] 

153 

154 if not dates: 

155 return {"total_days": 0, "start_date": None, "end_date": None, "display_text": "データなし"} 

156 

157 # 最古と最新の日付を取得 

158 start_date = min(dates) 

159 end_date = max(dates) 

160 

161 # 日数を計算 

162 start_dt = datetime.datetime.fromisoformat(start_date) 

163 end_dt = datetime.datetime.fromisoformat(end_date) 

164 total_days = (end_dt - start_dt).days + 1 

165 

166 # 表示テキストを生成 

167 if total_days == 1: 

168 display_text = f"過去1日間({start_date.replace('-', '年', 1).replace('-', '月', 1)}日)" 

169 else: 

170 start_display = start_date.replace("-", "年", 1).replace("-", "月", 1) + "日" 

171 display_text = f"過去{total_days}日間({start_display}〜)" 

172 

173 return { 

174 "total_days": total_days, 

175 "start_date": start_date, 

176 "end_date": end_date, 

177 "display_text": display_text, 

178 } 

179 

180 

181def _extract_time_data(day_data: dict, key: str) -> float | None: 

182 """時刻データを抽出して時間形式に変換""" 

183 if not day_data.get(key): 

184 return None 

185 try: 

186 dt = datetime.datetime.fromisoformat(day_data[key].replace("Z", "+00:00")) 

187 return dt.hour + dt.minute / 60.0 

188 except (ValueError, TypeError): 

189 return None 

190 

191 

192def _collect_sensor_data_by_type(operation_metrics: list[dict], operation_type: str) -> dict: 

193 """操作タイプ別にセンサーデータを収集""" 

194 sensor_data = { 

195 "open_lux": [], 

196 "close_lux": [], 

197 "open_solar_rad": [], 

198 "close_solar_rad": [], 

199 "open_altitude": [], 

200 "close_altitude": [], 

201 } 

202 

203 for op_data in operation_metrics: 

204 if op_data.get("operation_type") == operation_type: 

205 action = op_data.get("action") 

206 if action in ["open", "close"]: 

207 for sensor_type in ["lux", "solar_rad", "altitude"]: 

208 if op_data.get(sensor_type) is not None: 

209 sensor_data[f"{action}_{sensor_type}"].append(op_data[sensor_type]) 

210 

211 return sensor_data 

212 

213 

214def generate_statistics(operation_metrics: list[dict], failure_metrics: list[dict]) -> dict: 

215 """メトリクスデータから統計情報を生成""" 

216 if not operation_metrics: 

217 return { 

218 "total_days": 0, 

219 "open_times": [], 

220 "close_times": [], 

221 "auto_sensor_data": { 

222 "open_lux": [], 

223 "close_lux": [], 

224 "open_solar_rad": [], 

225 "close_solar_rad": [], 

226 "open_altitude": [], 

227 "close_altitude": [], 

228 }, 

229 "manual_sensor_data": { 

230 "open_lux": [], 

231 "close_lux": [], 

232 "open_solar_rad": [], 

233 "close_solar_rad": [], 

234 "open_altitude": [], 

235 "close_altitude": [], 

236 }, 

237 "manual_open_total": 0, 

238 "manual_close_total": 0, 

239 "auto_open_total": 0, 

240 "auto_close_total": 0, 

241 "failure_total": len(failure_metrics), 

242 } 

243 

244 # 日付ごとの最後の操作時刻を取得(時刻分析用) 

245 daily_last_operations = {} 

246 for op_data in operation_metrics: 

247 date = op_data.get("date") 

248 action = op_data.get("action") 

249 timestamp = op_data.get("timestamp") 

250 

251 if date and action and timestamp: 

252 key = f"{date}_{action}" 

253 # より新しい時刻で上書き(最後の操作時刻を保持) 

254 daily_last_operations[key] = timestamp 

255 

256 # 時刻データを収集(最後の操作時刻のみ) 

257 open_times = [] 

258 close_times = [] 

259 

260 for key, timestamp in daily_last_operations.items(): 

261 if ( 

262 key.endswith("_open") 

263 and (t := _extract_time_data({"timestamp": timestamp}, "timestamp")) is not None 

264 ): 

265 open_times.append(t) 

266 elif ( 

267 key.endswith("_close") 

268 and (t := _extract_time_data({"timestamp": timestamp}, "timestamp")) is not None 

269 ): 

270 close_times.append(t) 

271 

272 # センサーデータを操作タイプ別に収集 

273 auto_sensor_data = _collect_sensor_data_by_type(operation_metrics, "auto") 

274 auto_sensor_data.update(_collect_sensor_data_by_type(operation_metrics, "schedule")) 

275 manual_sensor_data = _collect_sensor_data_by_type(operation_metrics, "manual") 

276 

277 # カウント系データを集計 

278 manual_open_total = sum( 

279 1 for op in operation_metrics if op.get("operation_type") == "manual" and op.get("action") == "open" 

280 ) 

281 manual_close_total = sum( 

282 1 for op in operation_metrics if op.get("operation_type") == "manual" and op.get("action") == "close" 

283 ) 

284 auto_open_total = sum( 

285 1 

286 for op in operation_metrics 

287 if op.get("operation_type") in ["auto", "schedule"] and op.get("action") == "open" 

288 ) 

289 auto_close_total = sum( 

290 1 

291 for op in operation_metrics 

292 if op.get("operation_type") in ["auto", "schedule"] and op.get("action") == "close" 

293 ) 

294 

295 # 日数を計算 

296 unique_dates = {op.get("date") for op in operation_metrics if op.get("date")} 

297 

298 return { 

299 "total_days": len(unique_dates), 

300 "open_times": open_times, 

301 "close_times": close_times, 

302 "auto_sensor_data": auto_sensor_data, 

303 "manual_sensor_data": manual_sensor_data, 

304 "manual_open_total": manual_open_total, 

305 "manual_close_total": manual_close_total, 

306 "auto_open_total": auto_open_total, 

307 "auto_close_total": auto_close_total, 

308 "failure_total": len(failure_metrics), 

309 } 

310 

311 

312def generate_metrics_html(stats: dict, operation_metrics: list[dict], data_period: dict) -> str: 

313 """Bulma CSSを使用したメトリクスHTMLを生成""" 

314 # JavaScript用データを準備 

315 chart_data = { 

316 "open_times": stats["open_times"], 

317 "close_times": stats["close_times"], 

318 "auto_sensor_data": stats["auto_sensor_data"], 

319 "manual_sensor_data": stats["manual_sensor_data"], 

320 "time_series": prepare_time_series_data(operation_metrics), 

321 } 

322 

323 chart_data_json = json.dumps(chart_data) 

324 

325 # URL_PREFIXを取得してfaviconパスを構築 

326 favicon_path = f"{my_lib.webapp.config.URL_PREFIX}/favicon.ico" 

327 

328 return f""" 

329<!DOCTYPE html> 

330<html> 

331<head> 

332 <meta charset="utf-8"> 

333 <meta name="viewport" content="width=device-width, initial-scale=1"> 

334 <title>シャッター メトリクス ダッシュボード</title> 

335 <link rel="icon" type="image/x-icon" href="{favicon_path}"> 

336 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"> 

337 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> 

338 <script src="https://cdn.jsdelivr.net/npm/@sgratzl/chartjs-chart-boxplot"></script> 

339 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> 

340 <style> 

341 .metrics-card {{ margin-bottom: 1rem; }} 

342 @media (max-width: 768px) {{ 

343 .metrics-card {{ margin-bottom: 0.75rem; }} 

344 }} 

345 .stat-number {{ font-size: 2rem; font-weight: bold; }} 

346 .chart-container {{ position: relative; height: 350px; margin: 0.5rem 0; }} 

347 @media (max-width: 768px) {{ 

348 .chart-container {{ height: 300px; margin: 0.25rem 0; }} 

349 .container.is-fluid {{ padding: 0.25rem !important; }} 

350 .section {{ padding: 0.5rem 0.25rem !important; }} 

351 .card {{ margin-bottom: 1rem !important; }} 

352 .columns {{ margin: 0 !important; }} 

353 .column {{ padding: 0.25rem !important; }} 

354 }} 

355 .japanese-font {{ 

356 font-family: "Hiragino Sans", "Hiragino Kaku Gothic ProN", 

357 "Noto Sans CJK JP", "Yu Gothic", sans-serif; 

358 }} 

359 .permalink-header {{ 

360 position: relative; 

361 display: inline-block; 

362 }} 

363 .permalink-icon {{ 

364 opacity: 0; 

365 transition: opacity 0.2s ease-in-out; 

366 cursor: pointer; 

367 color: #4a90e2; 

368 margin-left: 0.5rem; 

369 font-size: 0.8em; 

370 }} 

371 .permalink-header:hover .permalink-icon {{ 

372 opacity: 1; 

373 }} 

374 .permalink-icon:hover {{ 

375 color: #357abd; 

376 }} 

377 </style> 

378</head> 

379<body class="japanese-font"> 

380 <div class="container is-fluid" style="padding: 0.5rem;"> 

381 <section class="section" style="padding: 1rem 0.5rem;"> 

382 <div class="container" style="max-width: 100%; padding: 0;"> 

383 <h1 class="title is-2 has-text-centered"> 

384 <span class="icon is-large"><i class="fas fa-chart-line"></i></span> 

385 シャッター メトリクス ダッシュボード 

386 </h1> 

387 <p class="subtitle has-text-centered">{data_period["display_text"]}のシャッター操作統計</p> 

388 

389 <!-- 基本統計 --> 

390 {generate_basic_stats_section(stats)} 

391 

392 <!-- 時刻分析 --> 

393 {generate_time_analysis_section()} 

394 

395 <!-- 時系列データ分析 --> 

396 {generate_time_series_section()} 

397 

398 <!-- センサーデータ分析 --> 

399 {generate_sensor_analysis_section()} 

400 </div> 

401 </section> 

402 </div> 

403 

404 <script> 

405 const chartData = {chart_data_json}; 

406 

407 // チャート生成 

408 generateTimeCharts(); 

409 generateTimeSeriesCharts(); 

410 generateAutoSensorCharts(); 

411 generateManualSensorCharts(); 

412 

413 // パーマリンク機能を初期化 

414 initializePermalinks(); 

415 

416 {generate_chart_javascript()} 

417 </script> 

418</html> 

419 """ 

420 

421 

422def _extract_daily_last_operations(operation_metrics: list[dict]) -> dict: 

423 """日付ごとの最後の操作時刻とセンサーデータを取得""" 

424 daily_last_operations = {} 

425 

426 for op_data in operation_metrics: 

427 date = op_data.get("date") 

428 action = op_data.get("action") 

429 timestamp = op_data.get("timestamp") 

430 

431 if date and action and timestamp: 

432 key = f"{date}_{action}" 

433 # より新しい時刻で上書き 

434 if key not in daily_last_operations or timestamp > daily_last_operations[key]["timestamp"]: 

435 daily_last_operations[key] = { 

436 "timestamp": timestamp, 

437 "lux": op_data.get("lux"), 

438 "solar_rad": op_data.get("solar_rad"), 

439 "altitude": op_data.get("altitude"), 

440 } 

441 

442 return daily_last_operations 

443 

444 

445def _extract_daily_data(date: str, action: str, daily_last_operations: dict) -> tuple[float | None, ...]: 

446 """指定した日付と操作の時刻とセンサーデータを抽出""" 

447 key = f"{date}_{action}" 

448 time_val = None 

449 lux_val = None 

450 solar_rad_val = None 

451 altitude_val = None 

452 

453 if key in daily_last_operations: 

454 try: 

455 dt = datetime.datetime.fromisoformat( 

456 daily_last_operations[key]["timestamp"].replace("Z", "+00:00") 

457 ) 

458 time_val = dt.hour + dt.minute / 60.0 

459 lux_val = daily_last_operations[key]["lux"] 

460 solar_rad_val = daily_last_operations[key]["solar_rad"] 

461 altitude_val = daily_last_operations[key]["altitude"] 

462 except (ValueError, TypeError): 

463 pass 

464 

465 return time_val, lux_val, solar_rad_val, altitude_val 

466 

467 

468def prepare_time_series_data(operation_metrics: list[dict]) -> dict: 

469 """時系列データを準備""" 

470 daily_last_operations = _extract_daily_last_operations(operation_metrics) 

471 

472 # 日付リストを生成 

473 unique_dates = sorted({op.get("date") for op in operation_metrics if op.get("date")}) 

474 

475 dates = [] 

476 open_times = [] 

477 close_times = [] 

478 open_lux = [] 

479 close_lux = [] 

480 open_solar_rad = [] 

481 close_solar_rad = [] 

482 open_altitude = [] 

483 close_altitude = [] 

484 

485 for date in unique_dates: 

486 dates.append(date) 

487 

488 # その日の最後の開操作時刻とセンサーデータ 

489 open_time, open_lux_val, open_solar_rad_val, open_altitude_val = _extract_daily_data( 

490 date, "open", daily_last_operations 

491 ) 

492 

493 # その日の最後の閉操作時刻とセンサーデータ 

494 close_time, close_lux_val, close_solar_rad_val, close_altitude_val = _extract_daily_data( 

495 date, "close", daily_last_operations 

496 ) 

497 

498 open_times.append(open_time) 

499 close_times.append(close_time) 

500 open_lux.append(open_lux_val) 

501 close_lux.append(close_lux_val) 

502 open_solar_rad.append(open_solar_rad_val) 

503 close_solar_rad.append(close_solar_rad_val) 

504 open_altitude.append(open_altitude_val) 

505 close_altitude.append(close_altitude_val) 

506 

507 return { 

508 "dates": dates, 

509 "open_times": open_times, 

510 "close_times": close_times, 

511 "open_lux": open_lux, 

512 "close_lux": close_lux, 

513 "open_solar_rad": open_solar_rad, 

514 "close_solar_rad": close_solar_rad, 

515 "open_altitude": open_altitude, 

516 "close_altitude": close_altitude, 

517 } 

518 

519 

520def generate_basic_stats_section(stats: dict) -> str: 

521 """基本統計セクションのHTML生成""" 

522 return f""" 

523 <div class="section"> 

524 <h2 class="title is-4 permalink-header" id="basic-stats"> 

525 <span class="icon"><i class="fas fa-chart-bar"></i></span> 

526 基本統計 

527 <span class="permalink-icon" onclick="copyPermalink('basic-stats')"> 

528 <i class="fas fa-link"></i> 

529 </span> 

530 </h2> 

531 

532 <div class="columns"> 

533 <div class="column"> 

534 <div class="card metrics-card"> 

535 <div class="card-header"> 

536 <p class="card-header-title">操作回数</p> 

537 </div> 

538 <div class="card-content"> 

539 <div class="columns is-multiline"> 

540 <div class="column is-2"> 

541 <div class="has-text-centered"> 

542 <p class="heading">👆 手動開操作 ☀️</p> 

543 <p class="stat-number has-text-success">{stats["manual_open_total"]:,}</p> 

544 </div> 

545 </div> 

546 <div class="column is-2"> 

547 <div class="has-text-centered"> 

548 <p class="heading">👆 手動閉操作 🌙</p> 

549 <p class="stat-number has-text-info">{stats["manual_close_total"]:,}</p> 

550 </div> 

551 </div> 

552 <div class="column is-2"> 

553 <div class="has-text-centered"> 

554 <p class="heading">🤖 自動開操作 ☀️</p> 

555 <p class="stat-number has-text-success">{stats["auto_open_total"]:,}</p> 

556 </div> 

557 </div> 

558 <div class="column is-2"> 

559 <div class="has-text-centered"> 

560 <p class="heading">🤖 自動閉操作 🌙</p> 

561 <p class="stat-number has-text-info">{stats["auto_close_total"]:,}</p> 

562 </div> 

563 </div> 

564 <div class="column is-2"> 

565 <div class="has-text-centered"> 

566 <p class="heading">制御失敗</p> 

567 <p class="stat-number has-text-danger">{stats["failure_total"]:,}</p> 

568 </div> 

569 </div> 

570 <div class="column is-2"> 

571 <div class="has-text-centered"> 

572 <p class="heading">データ収集日数</p> 

573 <p class="stat-number has-text-primary">{stats["total_days"]:,}</p> 

574 </div> 

575 </div> 

576 </div> 

577 </div> 

578 </div> 

579 </div> 

580 </div> 

581 </div> 

582 """ 

583 

584 

585def generate_time_analysis_section() -> str: 

586 """時刻分析セクションのHTML生成""" 

587 return """ 

588 <div class="section"> 

589 <h2 class="title is-4 permalink-header" id="time-analysis"> 

590 <span class="icon"><i class="fas fa-clock"></i></span> 時刻分析 

591 <span class="permalink-icon" onclick="copyPermalink('time-analysis')"> 

592 <i class="fas fa-link"></i> 

593 </span> 

594 </h2> 

595 

596 <div class="columns"> 

597 <div class="column is-half"> 

598 <div class="card metrics-card"> 

599 <div class="card-header"> 

600 <p class="card-header-title permalink-header" id="open-time-histogram"> 

601 ☀️ 開操作時刻の頻度分布 

602 <span class="permalink-icon" onclick="copyPermalink('open-time-histogram')"> 

603 <i class="fas fa-link"></i> 

604 </span> 

605 </p> 

606 </div> 

607 <div class="card-content"> 

608 <div class="chart-container"> 

609 <canvas id="openTimeHistogramChart"></canvas> 

610 </div> 

611 </div> 

612 </div> 

613 </div> 

614 <div class="column is-half"> 

615 <div class="card metrics-card"> 

616 <div class="card-header"> 

617 <p class="card-header-title permalink-header" id="close-time-histogram"> 

618 🌙 閉操作時刻の頻度分布 

619 <span class="permalink-icon" onclick="copyPermalink('close-time-histogram')"> 

620 <i class="fas fa-link"></i> 

621 </span> 

622 </p> 

623 </div> 

624 <div class="card-content"> 

625 <div class="chart-container"> 

626 <canvas id="closeTimeHistogramChart"></canvas> 

627 </div> 

628 </div> 

629 </div> 

630 </div> 

631 </div> 

632 </div> 

633 """ 

634 

635 

636def generate_time_series_section() -> str: 

637 """時系列データ分析セクションのHTML生成""" 

638 return """ 

639 <div class="section"> 

640 <h2 class="title is-4 permalink-header" id="time-series"> 

641 <span class="icon"><i class="fas fa-chart-line"></i></span> 時系列データ分析 

642 <span class="permalink-icon" onclick="copyPermalink('time-series')"> 

643 <i class="fas fa-link"></i> 

644 </span> 

645 </h2> 

646 

647 <div class="columns"> 

648 <div class="column"> 

649 <div class="card metrics-card"> 

650 <div class="card-header"> 

651 <p class="card-header-title permalink-header" id="time-series-chart"> 

652 🕐 操作時刻の時系列遷移 

653 <span class="permalink-icon" onclick="copyPermalink('time-series-chart')"> 

654 <i class="fas fa-link"></i> 

655 </span> 

656 </p> 

657 </div> 

658 <div class="card-content"> 

659 <div class="chart-container"> 

660 <canvas id="timeSeriesChart"></canvas> 

661 </div> 

662 </div> 

663 </div> 

664 </div> 

665 </div> 

666 

667 <div class="columns"> 

668 <div class="column"> 

669 <div class="card metrics-card"> 

670 <div class="card-header"> 

671 <p class="card-header-title permalink-header" id="lux-time-series"> 

672 💡 照度データの時系列遷移 

673 <span class="permalink-icon" onclick="copyPermalink('lux-time-series')"> 

674 <i class="fas fa-link"></i> 

675 </span> 

676 </p> 

677 </div> 

678 <div class="card-content"> 

679 <div class="chart-container"> 

680 <canvas id="luxTimeSeriesChart"></canvas> 

681 </div> 

682 </div> 

683 </div> 

684 </div> 

685 </div> 

686 

687 <div class="columns"> 

688 <div class="column"> 

689 <div class="card metrics-card"> 

690 <div class="card-header"> 

691 <p class="card-header-title permalink-header" id="solar-rad-time-series"> 

692 ☀️ 日射データの時系列遷移 

693 <span class="permalink-icon" onclick="copyPermalink('solar-rad-time-series')"> 

694 <i class="fas fa-link"></i> 

695 </span> 

696 </p> 

697 </div> 

698 <div class="card-content"> 

699 <div class="chart-container"> 

700 <canvas id="solarRadTimeSeriesChart"></canvas> 

701 </div> 

702 </div> 

703 </div> 

704 </div> 

705 </div> 

706 

707 <div class="columns"> 

708 <div class="column"> 

709 <div class="card metrics-card"> 

710 <div class="card-header"> 

711 <p class="card-header-title permalink-header" id="altitude-time-series"> 

712 📐 太陽高度の時系列遷移 

713 <span class="permalink-icon" onclick="copyPermalink('altitude-time-series')"> 

714 <i class="fas fa-link"></i> 

715 </span> 

716 </p> 

717 </div> 

718 <div class="card-content"> 

719 <div class="chart-container"> 

720 <canvas id="altitudeTimeSeriesChart"></canvas> 

721 </div> 

722 </div> 

723 </div> 

724 </div> 

725 </div> 

726 </div> 

727 """ 

728 

729 

730def generate_sensor_analysis_section() -> str: 

731 """センサーデータ分析セクションのHTML生成""" 

732 return """ 

733 <div class="section"> 

734 <h2 class="title is-4 permalink-header" id="auto-sensor-analysis"> 

735 <span class="icon"><i class="fas fa-robot"></i></span> センサーデータ分析(自動操作) 

736 <span class="permalink-icon" onclick="copyPermalink('auto-sensor-analysis')"> 

737 <i class="fas fa-link"></i> 

738 </span> 

739 </h2> 

740 

741 <!-- 照度データ --> 

742 <div class="columns"> 

743 <div class="column is-half"> 

744 <div class="card metrics-card"> 

745 <div class="card-header"> 

746 <p class="card-header-title permalink-header" id="auto-open-lux"> 

747 🤖 自動開操作時の照度データ ☀️ 

748 <span class="permalink-icon" onclick="copyPermalink('auto-open-lux')"> 

749 <i class="fas fa-link"></i> 

750 </span> 

751 </p> 

752 </div> 

753 <div class="card-content"> 

754 <div class="chart-container"> 

755 <canvas id="autoOpenLuxChart"></canvas> 

756 </div> 

757 </div> 

758 </div> 

759 </div> 

760 <div class="column is-half"> 

761 <div class="card metrics-card"> 

762 <div class="card-header"> 

763 <p class="card-header-title permalink-header" id="auto-close-lux"> 

764 🤖 自動閉操作時の照度データ 🌙 

765 <span class="permalink-icon" onclick="copyPermalink('auto-close-lux')"> 

766 <i class="fas fa-link"></i> 

767 </span> 

768 </p> 

769 </div> 

770 <div class="card-content"> 

771 <div class="chart-container"> 

772 <canvas id="autoCloseLuxChart"></canvas> 

773 </div> 

774 </div> 

775 </div> 

776 </div> 

777 </div> 

778 

779 <!-- 日射データ --> 

780 <div class="columns"> 

781 <div class="column is-half"> 

782 <div class="card metrics-card"> 

783 <div class="card-header"> 

784 <p class="card-header-title permalink-header" id="auto-open-solar-rad"> 

785 🤖 自動開操作時の日射データ ☀️ 

786 <span class="permalink-icon" onclick="copyPermalink('auto-open-solar-rad')"> 

787 <i class="fas fa-link"></i> 

788 </span> 

789 </p> 

790 </div> 

791 <div class="card-content"> 

792 <div class="chart-container"> 

793 <canvas id="autoOpenSolarRadChart"></canvas> 

794 </div> 

795 </div> 

796 </div> 

797 </div> 

798 <div class="column is-half"> 

799 <div class="card metrics-card"> 

800 <div class="card-header"> 

801 <p class="card-header-title permalink-header" id="auto-close-solar-rad"> 

802 🤖 自動閉操作時の日射データ 🌙 

803 <span class="permalink-icon" onclick="copyPermalink('auto-close-solar-rad')"> 

804 <i class="fas fa-link"></i> 

805 </span> 

806 </p> 

807 </div> 

808 <div class="card-content"> 

809 <div class="chart-container"> 

810 <canvas id="autoCloseSolarRadChart"></canvas> 

811 </div> 

812 </div> 

813 </div> 

814 </div> 

815 </div> 

816 

817 <!-- 太陽高度データ --> 

818 <div class="columns"> 

819 <div class="column is-half"> 

820 <div class="card metrics-card"> 

821 <div class="card-header"> 

822 <p class="card-header-title permalink-header" id="auto-open-altitude"> 

823 🤖 自動開操作時の太陽高度データ ☀️ 

824 <span class="permalink-icon" onclick="copyPermalink('auto-open-altitude')"> 

825 <i class="fas fa-link"></i> 

826 </span> 

827 </p> 

828 </div> 

829 <div class="card-content"> 

830 <div class="chart-container"> 

831 <canvas id="autoOpenAltitudeChart"></canvas> 

832 </div> 

833 </div> 

834 </div> 

835 </div> 

836 <div class="column is-half"> 

837 <div class="card metrics-card"> 

838 <div class="card-header"> 

839 <p class="card-header-title permalink-header" id="auto-close-altitude"> 

840 🤖 自動閉操作時の太陽高度データ 🌙 

841 <span class="permalink-icon" onclick="copyPermalink('auto-close-altitude')"> 

842 <i class="fas fa-link"></i> 

843 </span> 

844 </p> 

845 </div> 

846 <div class="card-content"> 

847 <div class="chart-container"> 

848 <canvas id="autoCloseAltitudeChart"></canvas> 

849 </div> 

850 </div> 

851 </div> 

852 </div> 

853 </div> 

854 </div> 

855 

856 <div class="section"> 

857 <h2 class="title is-4 permalink-header" id="manual-sensor-analysis"> 

858 <span class="icon"><i class="fas fa-hand-paper"></i></span> センサーデータ分析(手動操作) 

859 <span class="permalink-icon" onclick="copyPermalink('manual-sensor-analysis')"> 

860 <i class="fas fa-link"></i> 

861 </span> 

862 </h2> 

863 

864 <!-- 照度データ --> 

865 <div class="columns"> 

866 <div class="column is-half"> 

867 <div class="card metrics-card"> 

868 <div class="card-header"> 

869 <p class="card-header-title permalink-header" id="manual-open-lux"> 

870 👆 手動開操作時の照度データ ☀️ 

871 <span class="permalink-icon" onclick="copyPermalink('manual-open-lux')"> 

872 <i class="fas fa-link"></i> 

873 </span> 

874 </p> 

875 </div> 

876 <div class="card-content"> 

877 <div class="chart-container"> 

878 <canvas id="manualOpenLuxChart"></canvas> 

879 </div> 

880 </div> 

881 </div> 

882 </div> 

883 <div class="column is-half"> 

884 <div class="card metrics-card"> 

885 <div class="card-header"> 

886 <p class="card-header-title permalink-header" id="manual-close-lux"> 

887 👆 手動閉操作時の照度データ 🌙 

888 <span class="permalink-icon" onclick="copyPermalink('manual-close-lux')"> 

889 <i class="fas fa-link"></i> 

890 </span> 

891 </p> 

892 </div> 

893 <div class="card-content"> 

894 <div class="chart-container"> 

895 <canvas id="manualCloseLuxChart"></canvas> 

896 </div> 

897 </div> 

898 </div> 

899 </div> 

900 </div> 

901 

902 <!-- 日射データ --> 

903 <div class="columns"> 

904 <div class="column is-half"> 

905 <div class="card metrics-card"> 

906 <div class="card-header"> 

907 <p class="card-header-title permalink-header" id="manual-open-solar-rad"> 

908 👆 手動開操作時の日射データ ☀️ 

909 <span class="permalink-icon" onclick="copyPermalink('manual-open-solar-rad')"> 

910 <i class="fas fa-link"></i> 

911 </span> 

912 </p> 

913 </div> 

914 <div class="card-content"> 

915 <div class="chart-container"> 

916 <canvas id="manualOpenSolarRadChart"></canvas> 

917 </div> 

918 </div> 

919 </div> 

920 </div> 

921 <div class="column is-half"> 

922 <div class="card metrics-card"> 

923 <div class="card-header"> 

924 <p class="card-header-title permalink-header" id="manual-close-solar-rad"> 

925 👆 手動閉操作時の日射データ 🌙 

926 <span class="permalink-icon" onclick="copyPermalink('manual-close-solar-rad')"> 

927 <i class="fas fa-link"></i> 

928 </span> 

929 </p> 

930 </div> 

931 <div class="card-content"> 

932 <div class="chart-container"> 

933 <canvas id="manualCloseSolarRadChart"></canvas> 

934 </div> 

935 </div> 

936 </div> 

937 </div> 

938 </div> 

939 

940 <!-- 太陽高度データ --> 

941 <div class="columns"> 

942 <div class="column is-half"> 

943 <div class="card metrics-card"> 

944 <div class="card-header"> 

945 <p class="card-header-title permalink-header" id="manual-open-altitude"> 

946 👆 手動開操作時の太陽高度データ ☀️ 

947 <span class="permalink-icon" onclick="copyPermalink('manual-open-altitude')"> 

948 <i class="fas fa-link"></i> 

949 </span> 

950 </p> 

951 </div> 

952 <div class="card-content"> 

953 <div class="chart-container"> 

954 <canvas id="manualOpenAltitudeChart"></canvas> 

955 </div> 

956 </div> 

957 </div> 

958 </div> 

959 <div class="column is-half"> 

960 <div class="card metrics-card"> 

961 <div class="card-header"> 

962 <p class="card-header-title permalink-header" id="manual-close-altitude"> 

963 👆 手動閉操作時の太陽高度データ 🌙 

964 <span class="permalink-icon" onclick="copyPermalink('manual-close-altitude')"> 

965 <i class="fas fa-link"></i> 

966 </span> 

967 </p> 

968 </div> 

969 <div class="card-content"> 

970 <div class="chart-container"> 

971 <canvas id="manualCloseAltitudeChart"></canvas> 

972 </div> 

973 </div> 

974 </div> 

975 </div> 

976 </div> 

977 </div> 

978 """ 

979 

980 

981def generate_chart_javascript() -> str: 

982 """チャート生成用JavaScriptを生成""" 

983 return """ 

984 function initializePermalinks() { 

985 // ページ読み込み時にハッシュがある場合はスクロール 

986 if (window.location.hash) { 

987 const element = document.querySelector(window.location.hash); 

988 if (element) { 

989 setTimeout(() => { 

990 element.scrollIntoView({ behavior: 'smooth', block: 'start' }); 

991 }, 500); // チャート描画完了を待つ 

992 } 

993 } 

994 } 

995 

996 function copyPermalink(sectionId) { 

997 const url = window.location.origin + window.location.pathname + '#' + sectionId; 

998 

999 // Clipboard APIを使用してURLをコピー 

1000 if (navigator.clipboard && window.isSecureContext) { 

1001 navigator.clipboard.writeText(url).then(() => { 

1002 showCopyNotification(); 

1003 }).catch(err => { 

1004 console.error('Failed to copy: ', err); 

1005 fallbackCopyToClipboard(url); 

1006 }); 

1007 } else { 

1008 // フォールバック 

1009 fallbackCopyToClipboard(url); 

1010 } 

1011 

1012 // URLにハッシュを設定(履歴には残さない) 

1013 window.history.replaceState(null, null, '#' + sectionId); 

1014 } 

1015 

1016 function fallbackCopyToClipboard(text) { 

1017 const textArea = document.createElement("textarea"); 

1018 textArea.value = text; 

1019 textArea.style.position = "fixed"; 

1020 textArea.style.left = "-999999px"; 

1021 textArea.style.top = "-999999px"; 

1022 document.body.appendChild(textArea); 

1023 textArea.focus(); 

1024 textArea.select(); 

1025 

1026 try { 

1027 document.execCommand('copy'); 

1028 showCopyNotification(); 

1029 } catch (err) { 

1030 console.error('Fallback: Failed to copy', err); 

1031 // 最後の手段として、プロンプトでURLを表示 

1032 prompt('URLをコピーしてください:', text); 

1033 } 

1034 

1035 document.body.removeChild(textArea); 

1036 } 

1037 

1038 function showCopyNotification() { 

1039 // 通知要素を作成 

1040 const notification = document.createElement('div'); 

1041 notification.textContent = 'パーマリンクをコピーしました!'; 

1042 notification.style.cssText = ` 

1043 position: fixed; 

1044 top: 20px; 

1045 right: 20px; 

1046 background: #23d160; 

1047 color: white; 

1048 padding: 12px 20px; 

1049 border-radius: 4px; 

1050 z-index: 1000; 

1051 font-size: 14px; 

1052 font-weight: 500; 

1053 box-shadow: 0 2px 8px rgba(0,0,0,0.15); 

1054 transition: opacity 0.3s ease-in-out; 

1055 `; 

1056 

1057 document.body.appendChild(notification); 

1058 

1059 // 3秒後にフェードアウト 

1060 setTimeout(() => { 

1061 notification.style.opacity = '0'; 

1062 setTimeout(() => { 

1063 if (notification.parentNode) { 

1064 document.body.removeChild(notification); 

1065 } 

1066 }, 300); 

1067 }, 3000); 

1068 } 

1069 function generateTimeCharts() { 

1070 // 開操作時刻ヒストグラム 

1071 const openTimeHistogramCtx = document.getElementById('openTimeHistogramChart'); 

1072 if (openTimeHistogramCtx && chartData.open_times.length > 0) { 

1073 const bins = Array.from({length: 24}, (_, i) => i); 

1074 const openHist = Array(24).fill(0); 

1075 

1076 chartData.open_times.forEach(time => { 

1077 const hour = Math.floor(time); 

1078 if (hour >= 0 && hour < 24) openHist[hour]++; 

1079 }); 

1080 

1081 // 頻度を%に変換 

1082 const total = chartData.open_times.length; 

1083 const openHistPercent = openHist.map(count => total > 0 ? (count / total) * 100 : 0); 

1084 

1085 new Chart(openTimeHistogramCtx, { 

1086 type: 'bar', 

1087 data: { 

1088 labels: bins.map(h => h + ':00'), 

1089 datasets: [{ 

1090 label: '☀️ 開操作頻度', 

1091 data: openHistPercent, 

1092 backgroundColor: 'rgba(255, 206, 84, 0.7)', 

1093 borderColor: 'rgba(255, 206, 84, 1)', 

1094 borderWidth: 1 

1095 }] 

1096 }, 

1097 options: { 

1098 responsive: true, 

1099 maintainAspectRatio: false, 

1100 scales: { 

1101 y: { 

1102 beginAtZero: true, 

1103 max: 100, 

1104 title: { 

1105 display: true, 

1106 text: '頻度(%)' 

1107 }, 

1108 ticks: { 

1109 callback: function(value) { 

1110 return value + '%'; 

1111 } 

1112 } 

1113 }, 

1114 x: { 

1115 title: { 

1116 display: true, 

1117 text: '時刻' 

1118 } 

1119 } 

1120 } 

1121 } 

1122 }); 

1123 } 

1124 

1125 // 閉操作時刻ヒストグラム 

1126 const closeTimeHistogramCtx = document.getElementById('closeTimeHistogramChart'); 

1127 if (closeTimeHistogramCtx && chartData.close_times.length > 0) { 

1128 const bins = Array.from({length: 24}, (_, i) => i); 

1129 const closeHist = Array(24).fill(0); 

1130 

1131 chartData.close_times.forEach(time => { 

1132 const hour = Math.floor(time); 

1133 if (hour >= 0 && hour < 24) closeHist[hour]++; 

1134 }); 

1135 

1136 // 頻度を%に変換 

1137 const total = chartData.close_times.length; 

1138 const closeHistPercent = closeHist.map(count => total > 0 ? (count / total) * 100 : 0); 

1139 

1140 new Chart(closeTimeHistogramCtx, { 

1141 type: 'bar', 

1142 data: { 

1143 labels: bins.map(h => h + ':00'), 

1144 datasets: [{ 

1145 label: '🌙 閉操作頻度', 

1146 data: closeHistPercent, 

1147 backgroundColor: 'rgba(153, 102, 255, 0.7)', 

1148 borderColor: 'rgba(153, 102, 255, 1)', 

1149 borderWidth: 1 

1150 }] 

1151 }, 

1152 options: { 

1153 responsive: true, 

1154 maintainAspectRatio: false, 

1155 scales: { 

1156 y: { 

1157 beginAtZero: true, 

1158 max: 100, 

1159 title: { 

1160 display: true, 

1161 text: '頻度(%)' 

1162 }, 

1163 ticks: { 

1164 callback: function(value) { 

1165 return value + '%'; 

1166 } 

1167 } 

1168 }, 

1169 x: { 

1170 title: { 

1171 display: true, 

1172 text: '時刻' 

1173 } 

1174 } 

1175 } 

1176 } 

1177 }); 

1178 } 

1179 } 

1180 

1181 function generateTimeSeriesCharts() { 

1182 // 操作時刻の時系列グラフ 

1183 const timeSeriesCtx = document.getElementById('timeSeriesChart'); 

1184 if (timeSeriesCtx && chartData.time_series && chartData.time_series.dates.length > 0) { 

1185 new Chart(timeSeriesCtx, { 

1186 type: 'line', 

1187 data: { 

1188 labels: chartData.time_series.dates, 

1189 datasets: [ 

1190 { 

1191 label: '☀️ 開操作時刻', 

1192 data: chartData.time_series.open_times, 

1193 borderColor: 'rgba(255, 206, 84, 1)', 

1194 backgroundColor: 'rgba(255, 206, 84, 0.1)', 

1195 tension: 0.1, 

1196 spanGaps: true 

1197 }, 

1198 { 

1199 label: '🌙 閉操作時刻', 

1200 data: chartData.time_series.close_times, 

1201 borderColor: 'rgba(153, 102, 255, 1)', 

1202 backgroundColor: 'rgba(153, 102, 255, 0.1)', 

1203 tension: 0.1, 

1204 spanGaps: true 

1205 } 

1206 ] 

1207 }, 

1208 options: { 

1209 responsive: true, 

1210 maintainAspectRatio: false, 

1211 interaction: { 

1212 mode: 'index', 

1213 intersect: false 

1214 }, 

1215 scales: { 

1216 y: { 

1217 beginAtZero: true, 

1218 max: 24, 

1219 title: { 

1220 display: true, 

1221 text: '時刻' 

1222 }, 

1223 ticks: { 

1224 callback: function(value) { 

1225 const hour = Math.floor(value); 

1226 const minute = Math.round((value - hour) * 60); 

1227 return hour + ':' + (minute < 10 ? '0' : '') + minute; 

1228 } 

1229 } 

1230 }, 

1231 x: { 

1232 title: { 

1233 display: true, 

1234 text: '日付' 

1235 } 

1236 } 

1237 } 

1238 } 

1239 }); 

1240 } 

1241 

1242 // 照度の時系列グラフ 

1243 const luxTimeSeriesCtx = document.getElementById('luxTimeSeriesChart'); 

1244 if (luxTimeSeriesCtx && chartData.time_series && chartData.time_series.dates.length > 0) { 

1245 new Chart(luxTimeSeriesCtx, { 

1246 type: 'line', 

1247 data: { 

1248 labels: chartData.time_series.dates, 

1249 datasets: [ 

1250 { 

1251 label: '☀️ 開操作時照度', 

1252 data: chartData.time_series.open_lux, 

1253 borderColor: 'rgba(255, 206, 84, 1)', 

1254 backgroundColor: 'rgba(255, 206, 84, 0.1)', 

1255 tension: 0.1, 

1256 spanGaps: true 

1257 }, 

1258 { 

1259 label: '🌙 閉操作時照度', 

1260 data: chartData.time_series.close_lux, 

1261 borderColor: 'rgba(153, 102, 255, 1)', 

1262 backgroundColor: 'rgba(153, 102, 255, 0.1)', 

1263 tension: 0.1, 

1264 spanGaps: true 

1265 } 

1266 ] 

1267 }, 

1268 options: { 

1269 responsive: true, 

1270 maintainAspectRatio: false, 

1271 interaction: { 

1272 mode: 'index', 

1273 intersect: false 

1274 }, 

1275 scales: { 

1276 y: { 

1277 beginAtZero: true, 

1278 title: { 

1279 display: true, 

1280 text: '照度(lux)' 

1281 }, 

1282 ticks: { 

1283 callback: function(value) { 

1284 return value.toLocaleString(); 

1285 } 

1286 } 

1287 }, 

1288 x: { 

1289 title: { 

1290 display: true, 

1291 text: '日付' 

1292 } 

1293 } 

1294 } 

1295 } 

1296 }); 

1297 } 

1298 

1299 // 日射の時系列グラフ 

1300 const solarRadTimeSeriesCtx = document.getElementById('solarRadTimeSeriesChart'); 

1301 if (solarRadTimeSeriesCtx && chartData.time_series && chartData.time_series.dates.length > 0) { 

1302 new Chart(solarRadTimeSeriesCtx, { 

1303 type: 'line', 

1304 data: { 

1305 labels: chartData.time_series.dates, 

1306 datasets: [ 

1307 { 

1308 label: '☀️ 開操作時日射', 

1309 data: chartData.time_series.open_solar_rad, 

1310 borderColor: 'rgba(255, 159, 64, 1)', 

1311 backgroundColor: 'rgba(255, 159, 64, 0.1)', 

1312 tension: 0.1, 

1313 spanGaps: true 

1314 }, 

1315 { 

1316 label: '🌙 閉操作時日射', 

1317 data: chartData.time_series.close_solar_rad, 

1318 borderColor: 'rgba(75, 192, 192, 1)', 

1319 backgroundColor: 'rgba(75, 192, 192, 0.1)', 

1320 tension: 0.1, 

1321 spanGaps: true 

1322 } 

1323 ] 

1324 }, 

1325 options: { 

1326 responsive: true, 

1327 maintainAspectRatio: false, 

1328 interaction: { 

1329 mode: 'index', 

1330 intersect: false 

1331 }, 

1332 scales: { 

1333 y: { 

1334 beginAtZero: true, 

1335 title: { 

1336 display: true, 

1337 text: '日射(W/m²)' 

1338 } 

1339 }, 

1340 x: { 

1341 title: { 

1342 display: true, 

1343 text: '日付' 

1344 } 

1345 } 

1346 } 

1347 } 

1348 }); 

1349 } 

1350 

1351 // 太陽高度の時系列グラフ 

1352 const altitudeTimeSeriesCtx = document.getElementById('altitudeTimeSeriesChart'); 

1353 if (altitudeTimeSeriesCtx && chartData.time_series && chartData.time_series.dates.length > 0) { 

1354 new Chart(altitudeTimeSeriesCtx, { 

1355 type: 'line', 

1356 data: { 

1357 labels: chartData.time_series.dates, 

1358 datasets: [ 

1359 { 

1360 label: '☀️ 開操作時太陽高度', 

1361 data: chartData.time_series.open_altitude, 

1362 borderColor: 'rgba(255, 99, 132, 1)', 

1363 backgroundColor: 'rgba(255, 99, 132, 0.1)', 

1364 tension: 0.1, 

1365 spanGaps: true 

1366 }, 

1367 { 

1368 label: '🌙 閉操作時太陽高度', 

1369 data: chartData.time_series.close_altitude, 

1370 borderColor: 'rgba(54, 162, 235, 1)', 

1371 backgroundColor: 'rgba(54, 162, 235, 0.1)', 

1372 tension: 0.1, 

1373 spanGaps: true 

1374 } 

1375 ] 

1376 }, 

1377 options: { 

1378 responsive: true, 

1379 maintainAspectRatio: false, 

1380 interaction: { 

1381 mode: 'index', 

1382 intersect: false 

1383 }, 

1384 scales: { 

1385 y: { 

1386 title: { 

1387 display: true, 

1388 text: '太陽高度(度)' 

1389 } 

1390 }, 

1391 x: { 

1392 title: { 

1393 display: true, 

1394 text: '日付' 

1395 } 

1396 } 

1397 } 

1398 } 

1399 }); 

1400 } 

1401 } 

1402 

1403 function generateAutoSensorCharts() { 

1404 // ヒストグラム生成のヘルパー関数 

1405 function createHistogram(data, bins) { 

1406 const hist = Array(bins.length - 1).fill(0); 

1407 data.forEach(value => { 

1408 for (let i = 0; i < bins.length - 1; i++) { 

1409 if (value >= bins[i] && value < bins[i + 1]) { 

1410 hist[i]++; 

1411 break; 

1412 } 

1413 } 

1414 }); 

1415 return hist; 

1416 } 

1417 

1418 // 自動開操作時照度チャート 

1419 const autoOpenLuxCtx = document.getElementById('autoOpenLuxChart'); 

1420 if (autoOpenLuxCtx && chartData.auto_sensor_data.open_lux.length > 0) { 

1421 const minLux = Math.min(...chartData.auto_sensor_data.open_lux); 

1422 const maxLux = Math.max(...chartData.auto_sensor_data.open_lux); 

1423 const bins = Array.from({length: 21}, (_, i) => minLux + (maxLux - minLux) * i / 20); 

1424 const hist = createHistogram(chartData.auto_sensor_data.open_lux, bins); 

1425 const total = chartData.auto_sensor_data.open_lux.length; 

1426 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1427 

1428 new Chart(autoOpenLuxCtx, { 

1429 type: 'bar', 

1430 data: { 

1431 labels: bins.slice(0, -1).map(b => Math.round(b).toLocaleString()), 

1432 datasets: [{ 

1433 label: '🤖☀️ 自動開操作時照度頻度', 

1434 data: histPercent, 

1435 backgroundColor: 'rgba(255, 206, 84, 0.7)', 

1436 borderColor: 'rgba(255, 206, 84, 1)', 

1437 borderWidth: 1 

1438 }] 

1439 }, 

1440 options: { 

1441 responsive: true, 

1442 maintainAspectRatio: false, 

1443 scales: { 

1444 y: { 

1445 beginAtZero: true, 

1446 max: 100, 

1447 title: { 

1448 display: true, 

1449 text: '頻度(%)' 

1450 }, 

1451 ticks: { 

1452 callback: function(value) { 

1453 return value + '%'; 

1454 } 

1455 } 

1456 }, 

1457 x: { 

1458 title: { 

1459 display: true, 

1460 text: '照度(lux)' 

1461 } 

1462 } 

1463 } 

1464 } 

1465 }); 

1466 } 

1467 

1468 // 自動閉操作時照度チャート 

1469 const autoCloseLuxCtx = document.getElementById('autoCloseLuxChart'); 

1470 if (autoCloseLuxCtx && chartData.auto_sensor_data.close_lux.length > 0) { 

1471 const minLux = Math.min(...chartData.auto_sensor_data.close_lux); 

1472 const maxLux = Math.max(...chartData.auto_sensor_data.close_lux); 

1473 const bins = Array.from({length: 21}, (_, i) => minLux + (maxLux - minLux) * i / 20); 

1474 const hist = createHistogram(chartData.auto_sensor_data.close_lux, bins); 

1475 const total = chartData.auto_sensor_data.close_lux.length; 

1476 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1477 

1478 new Chart(autoCloseLuxCtx, { 

1479 type: 'bar', 

1480 data: { 

1481 labels: bins.slice(0, -1).map(b => Math.round(b).toLocaleString()), 

1482 datasets: [{ 

1483 label: '🤖🌙 自動閉操作時照度頻度', 

1484 data: histPercent, 

1485 backgroundColor: 'rgba(153, 102, 255, 0.7)', 

1486 borderColor: 'rgba(153, 102, 255, 1)', 

1487 borderWidth: 1 

1488 }] 

1489 }, 

1490 options: { 

1491 responsive: true, 

1492 maintainAspectRatio: false, 

1493 scales: { 

1494 y: { 

1495 beginAtZero: true, 

1496 max: 100, 

1497 title: { 

1498 display: true, 

1499 text: '頻度(%)' 

1500 }, 

1501 ticks: { 

1502 callback: function(value) { 

1503 return value + '%'; 

1504 } 

1505 } 

1506 }, 

1507 x: { 

1508 title: { 

1509 display: true, 

1510 text: '照度(lux)' 

1511 } 

1512 } 

1513 } 

1514 } 

1515 }); 

1516 } 

1517 

1518 // 自動開操作時日射チャート 

1519 const autoOpenSolarRadCtx = document.getElementById('autoOpenSolarRadChart'); 

1520 if (autoOpenSolarRadCtx && chartData.auto_sensor_data.open_solar_rad.length > 0) { 

1521 const minRad = Math.min(...chartData.auto_sensor_data.open_solar_rad); 

1522 const maxRad = Math.max(...chartData.auto_sensor_data.open_solar_rad); 

1523 const bins = Array.from({length: 21}, (_, i) => minRad + (maxRad - minRad) * i / 20); 

1524 const hist = createHistogram(chartData.auto_sensor_data.open_solar_rad, bins); 

1525 const total = chartData.auto_sensor_data.open_solar_rad.length; 

1526 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1527 

1528 new Chart(autoOpenSolarRadCtx, { 

1529 type: 'bar', 

1530 data: { 

1531 labels: bins.slice(0, -1).map(b => Math.round(b).toLocaleString()), 

1532 datasets: [{ 

1533 label: '🤖☀️ 自動開操作時日射頻度', 

1534 data: histPercent, 

1535 backgroundColor: 'rgba(255, 159, 64, 0.7)', 

1536 borderColor: 'rgba(255, 159, 64, 1)', 

1537 borderWidth: 1 

1538 }] 

1539 }, 

1540 options: { 

1541 responsive: true, 

1542 maintainAspectRatio: false, 

1543 scales: { 

1544 y: { 

1545 beginAtZero: true, 

1546 max: 100, 

1547 title: { 

1548 display: true, 

1549 text: '頻度(%)' 

1550 }, 

1551 ticks: { 

1552 callback: function(value) { 

1553 return value + '%'; 

1554 } 

1555 } 

1556 }, 

1557 x: { 

1558 title: { 

1559 display: true, 

1560 text: '日射(W/m²)' 

1561 } 

1562 } 

1563 } 

1564 } 

1565 }); 

1566 } 

1567 

1568 // 自動閉操作時日射チャート 

1569 const autoCloseSolarRadCtx = document.getElementById('autoCloseSolarRadChart'); 

1570 if (autoCloseSolarRadCtx && chartData.auto_sensor_data.close_solar_rad.length > 0) { 

1571 const minRad = Math.min(...chartData.auto_sensor_data.close_solar_rad); 

1572 const maxRad = Math.max(...chartData.auto_sensor_data.close_solar_rad); 

1573 const bins = Array.from({length: 21}, (_, i) => minRad + (maxRad - minRad) * i / 20); 

1574 const hist = createHistogram(chartData.auto_sensor_data.close_solar_rad, bins); 

1575 const total = chartData.auto_sensor_data.close_solar_rad.length; 

1576 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1577 

1578 new Chart(autoCloseSolarRadCtx, { 

1579 type: 'bar', 

1580 data: { 

1581 labels: bins.slice(0, -1).map(b => Math.round(b).toLocaleString()), 

1582 datasets: [{ 

1583 label: '🤖🌙 自動閉操作時日射頻度', 

1584 data: histPercent, 

1585 backgroundColor: 'rgba(75, 192, 192, 0.7)', 

1586 borderColor: 'rgba(75, 192, 192, 1)', 

1587 borderWidth: 1 

1588 }] 

1589 }, 

1590 options: { 

1591 responsive: true, 

1592 maintainAspectRatio: false, 

1593 scales: { 

1594 y: { 

1595 beginAtZero: true, 

1596 max: 100, 

1597 title: { 

1598 display: true, 

1599 text: '頻度(%)' 

1600 }, 

1601 ticks: { 

1602 callback: function(value) { 

1603 return value + '%'; 

1604 } 

1605 } 

1606 }, 

1607 x: { 

1608 title: { 

1609 display: true, 

1610 text: '日射(W/m²)' 

1611 } 

1612 } 

1613 } 

1614 } 

1615 }); 

1616 } 

1617 

1618 // 自動開操作時太陽高度チャート 

1619 const autoOpenAltitudeCtx = document.getElementById('autoOpenAltitudeChart'); 

1620 if (autoOpenAltitudeCtx && chartData.auto_sensor_data.open_altitude.length > 0) { 

1621 const minAlt = Math.min(...chartData.auto_sensor_data.open_altitude); 

1622 const maxAlt = Math.max(...chartData.auto_sensor_data.open_altitude); 

1623 const bins = Array.from({length: 21}, (_, i) => minAlt + (maxAlt - minAlt) * i / 20); 

1624 const hist = createHistogram(chartData.auto_sensor_data.open_altitude, bins); 

1625 const total = chartData.auto_sensor_data.open_altitude.length; 

1626 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1627 

1628 new Chart(autoOpenAltitudeCtx, { 

1629 type: 'bar', 

1630 data: { 

1631 labels: bins.slice(0, -1).map(b => Math.round(b * 10) / 10), 

1632 datasets: [{ 

1633 label: '🤖☀️ 自動開操作時太陽高度頻度', 

1634 data: histPercent, 

1635 backgroundColor: 'rgba(255, 99, 132, 0.7)', 

1636 borderColor: 'rgba(255, 99, 132, 1)', 

1637 borderWidth: 1 

1638 }] 

1639 }, 

1640 options: { 

1641 responsive: true, 

1642 maintainAspectRatio: false, 

1643 scales: { 

1644 y: { 

1645 beginAtZero: true, 

1646 max: 100, 

1647 title: { 

1648 display: true, 

1649 text: '頻度(%)' 

1650 }, 

1651 ticks: { 

1652 callback: function(value) { 

1653 return value + '%'; 

1654 } 

1655 } 

1656 }, 

1657 x: { 

1658 title: { 

1659 display: true, 

1660 text: '太陽高度(度)' 

1661 } 

1662 } 

1663 } 

1664 } 

1665 }); 

1666 } 

1667 

1668 // 自動閉操作時太陽高度チャート 

1669 const autoCloseAltitudeCtx = document.getElementById('autoCloseAltitudeChart'); 

1670 if (autoCloseAltitudeCtx && chartData.auto_sensor_data.close_altitude.length > 0) { 

1671 const minAlt = Math.min(...chartData.auto_sensor_data.close_altitude); 

1672 const maxAlt = Math.max(...chartData.auto_sensor_data.close_altitude); 

1673 const bins = Array.from({length: 21}, (_, i) => minAlt + (maxAlt - minAlt) * i / 20); 

1674 const hist = createHistogram(chartData.auto_sensor_data.close_altitude, bins); 

1675 const total = chartData.auto_sensor_data.close_altitude.length; 

1676 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1677 

1678 new Chart(autoCloseAltitudeCtx, { 

1679 type: 'bar', 

1680 data: { 

1681 labels: bins.slice(0, -1).map(b => Math.round(b * 10) / 10), 

1682 datasets: [{ 

1683 label: '🤖🌙 自動閉操作時太陽高度頻度', 

1684 data: histPercent, 

1685 backgroundColor: 'rgba(54, 162, 235, 0.7)', 

1686 borderColor: 'rgba(54, 162, 235, 1)', 

1687 borderWidth: 1 

1688 }] 

1689 }, 

1690 options: { 

1691 responsive: true, 

1692 maintainAspectRatio: false, 

1693 scales: { 

1694 y: { 

1695 beginAtZero: true, 

1696 max: 100, 

1697 title: { 

1698 display: true, 

1699 text: '頻度(%)' 

1700 }, 

1701 ticks: { 

1702 callback: function(value) { 

1703 return value + '%'; 

1704 } 

1705 } 

1706 }, 

1707 x: { 

1708 title: { 

1709 display: true, 

1710 text: '太陽高度(度)' 

1711 } 

1712 } 

1713 } 

1714 } 

1715 }); 

1716 } 

1717 } 

1718 

1719 function generateManualSensorCharts() { 

1720 // ヒストグラム生成のヘルパー関数 

1721 function createHistogram(data, bins) { 

1722 const hist = Array(bins.length - 1).fill(0); 

1723 data.forEach(value => { 

1724 for (let i = 0; i < bins.length - 1; i++) { 

1725 if (value >= bins[i] && value < bins[i + 1]) { 

1726 hist[i]++; 

1727 break; 

1728 } 

1729 } 

1730 }); 

1731 return hist; 

1732 } 

1733 

1734 // 手動開操作時照度チャート 

1735 const manualOpenLuxCtx = document.getElementById('manualOpenLuxChart'); 

1736 if (manualOpenLuxCtx && chartData.manual_sensor_data && 

1737 chartData.manual_sensor_data.open_lux && 

1738 chartData.manual_sensor_data.open_lux.length > 0) { 

1739 const minLux = Math.min(...chartData.manual_sensor_data.open_lux); 

1740 const maxLux = Math.max(...chartData.manual_sensor_data.open_lux); 

1741 const bins = Array.from({length: 21}, (_, i) => minLux + (maxLux - minLux) * i / 20); 

1742 const hist = createHistogram(chartData.manual_sensor_data.open_lux, bins); 

1743 const total = chartData.manual_sensor_data.open_lux.length; 

1744 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1745 

1746 new Chart(manualOpenLuxCtx, { 

1747 type: 'bar', 

1748 data: { 

1749 labels: bins.slice(0, -1).map(b => Math.round(b).toLocaleString()), 

1750 datasets: [{ 

1751 label: '👆☀️ 手動開操作時照度頻度', 

1752 data: histPercent, 

1753 backgroundColor: 'rgba(255, 206, 84, 0.7)', 

1754 borderColor: 'rgba(255, 206, 84, 1)', 

1755 borderWidth: 1 

1756 }] 

1757 }, 

1758 options: { 

1759 responsive: true, 

1760 maintainAspectRatio: false, 

1761 scales: { 

1762 y: { 

1763 beginAtZero: true, 

1764 max: 100, 

1765 title: { 

1766 display: true, 

1767 text: '頻度(%)' 

1768 }, 

1769 ticks: { 

1770 callback: function(value) { 

1771 return value + '%'; 

1772 } 

1773 } 

1774 }, 

1775 x: { 

1776 title: { 

1777 display: true, 

1778 text: '照度(lux)' 

1779 } 

1780 } 

1781 } 

1782 } 

1783 }); 

1784 } 

1785 

1786 // 手動閉操作時照度チャート 

1787 const manualCloseLuxCtx = document.getElementById('manualCloseLuxChart'); 

1788 if (manualCloseLuxCtx && chartData.manual_sensor_data && 

1789 chartData.manual_sensor_data.close_lux && 

1790 chartData.manual_sensor_data.close_lux.length > 0) { 

1791 const minLux = Math.min(...chartData.manual_sensor_data.close_lux); 

1792 const maxLux = Math.max(...chartData.manual_sensor_data.close_lux); 

1793 const bins = Array.from({length: 21}, (_, i) => minLux + (maxLux - minLux) * i / 20); 

1794 const hist = createHistogram(chartData.manual_sensor_data.close_lux, bins); 

1795 const total = chartData.manual_sensor_data.close_lux.length; 

1796 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1797 

1798 new Chart(manualCloseLuxCtx, { 

1799 type: 'bar', 

1800 data: { 

1801 labels: bins.slice(0, -1).map(b => Math.round(b).toLocaleString()), 

1802 datasets: [{ 

1803 label: '👆🌙 手動閉操作時照度頻度', 

1804 data: histPercent, 

1805 backgroundColor: 'rgba(153, 102, 255, 0.7)', 

1806 borderColor: 'rgba(153, 102, 255, 1)', 

1807 borderWidth: 1 

1808 }] 

1809 }, 

1810 options: { 

1811 responsive: true, 

1812 maintainAspectRatio: false, 

1813 scales: { 

1814 y: { 

1815 beginAtZero: true, 

1816 max: 100, 

1817 title: { 

1818 display: true, 

1819 text: '頻度(%)' 

1820 }, 

1821 ticks: { 

1822 callback: function(value) { 

1823 return value + '%'; 

1824 } 

1825 } 

1826 }, 

1827 x: { 

1828 title: { 

1829 display: true, 

1830 text: '照度(lux)' 

1831 } 

1832 } 

1833 } 

1834 } 

1835 }); 

1836 } 

1837 

1838 // 手動開操作時日射チャート 

1839 const manualOpenSolarRadCtx = document.getElementById('manualOpenSolarRadChart'); 

1840 if (manualOpenSolarRadCtx && chartData.manual_sensor_data && 

1841 chartData.manual_sensor_data.open_solar_rad && 

1842 chartData.manual_sensor_data.open_solar_rad.length > 0) { 

1843 const minRad = Math.min(...chartData.manual_sensor_data.open_solar_rad); 

1844 const maxRad = Math.max(...chartData.manual_sensor_data.open_solar_rad); 

1845 const bins = Array.from({length: 21}, (_, i) => minRad + (maxRad - minRad) * i / 20); 

1846 const hist = createHistogram(chartData.manual_sensor_data.open_solar_rad, bins); 

1847 const total = chartData.manual_sensor_data.open_solar_rad.length; 

1848 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1849 

1850 new Chart(manualOpenSolarRadCtx, { 

1851 type: 'bar', 

1852 data: { 

1853 labels: bins.slice(0, -1).map(b => Math.round(b).toLocaleString()), 

1854 datasets: [{ 

1855 label: '👆☀️ 手動開操作時日射頻度', 

1856 data: histPercent, 

1857 backgroundColor: 'rgba(255, 159, 64, 0.7)', 

1858 borderColor: 'rgba(255, 159, 64, 1)', 

1859 borderWidth: 1 

1860 }] 

1861 }, 

1862 options: { 

1863 responsive: true, 

1864 maintainAspectRatio: false, 

1865 scales: { 

1866 y: { 

1867 beginAtZero: true, 

1868 max: 100, 

1869 title: { 

1870 display: true, 

1871 text: '頻度(%)' 

1872 }, 

1873 ticks: { 

1874 callback: function(value) { 

1875 return value + '%'; 

1876 } 

1877 } 

1878 }, 

1879 x: { 

1880 title: { 

1881 display: true, 

1882 text: '日射(W/m²)' 

1883 } 

1884 } 

1885 } 

1886 } 

1887 }); 

1888 } 

1889 

1890 // 手動閉操作時日射チャート 

1891 const manualCloseSolarRadCtx = document.getElementById('manualCloseSolarRadChart'); 

1892 if (manualCloseSolarRadCtx && chartData.manual_sensor_data && 

1893 chartData.manual_sensor_data.close_solar_rad && 

1894 chartData.manual_sensor_data.close_solar_rad.length > 0) { 

1895 const minRad = Math.min(...chartData.manual_sensor_data.close_solar_rad); 

1896 const maxRad = Math.max(...chartData.manual_sensor_data.close_solar_rad); 

1897 const bins = Array.from({length: 21}, (_, i) => minRad + (maxRad - minRad) * i / 20); 

1898 const hist = createHistogram(chartData.manual_sensor_data.close_solar_rad, bins); 

1899 const total = chartData.manual_sensor_data.close_solar_rad.length; 

1900 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1901 

1902 new Chart(manualCloseSolarRadCtx, { 

1903 type: 'bar', 

1904 data: { 

1905 labels: bins.slice(0, -1).map(b => Math.round(b).toLocaleString()), 

1906 datasets: [{ 

1907 label: '👆🌙 手動閉操作時日射頻度', 

1908 data: histPercent, 

1909 backgroundColor: 'rgba(75, 192, 192, 0.7)', 

1910 borderColor: 'rgba(75, 192, 192, 1)', 

1911 borderWidth: 1 

1912 }] 

1913 }, 

1914 options: { 

1915 responsive: true, 

1916 maintainAspectRatio: false, 

1917 scales: { 

1918 y: { 

1919 beginAtZero: true, 

1920 max: 100, 

1921 title: { 

1922 display: true, 

1923 text: '頻度(%)' 

1924 }, 

1925 ticks: { 

1926 callback: function(value) { 

1927 return value + '%'; 

1928 } 

1929 } 

1930 }, 

1931 x: { 

1932 title: { 

1933 display: true, 

1934 text: '日射(W/m²)' 

1935 } 

1936 } 

1937 } 

1938 } 

1939 }); 

1940 } 

1941 

1942 // 手動開操作時太陽高度チャート 

1943 const manualOpenAltitudeCtx = document.getElementById('manualOpenAltitudeChart'); 

1944 if (manualOpenAltitudeCtx && chartData.manual_sensor_data && 

1945 chartData.manual_sensor_data.open_altitude && 

1946 chartData.manual_sensor_data.open_altitude.length > 0) { 

1947 const minAlt = Math.min(...chartData.manual_sensor_data.open_altitude); 

1948 const maxAlt = Math.max(...chartData.manual_sensor_data.open_altitude); 

1949 const bins = Array.from({length: 21}, (_, i) => minAlt + (maxAlt - minAlt) * i / 20); 

1950 const hist = createHistogram(chartData.manual_sensor_data.open_altitude, bins); 

1951 const total = chartData.manual_sensor_data.open_altitude.length; 

1952 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

1953 

1954 new Chart(manualOpenAltitudeCtx, { 

1955 type: 'bar', 

1956 data: { 

1957 labels: bins.slice(0, -1).map(b => Math.round(b * 10) / 10), 

1958 datasets: [{ 

1959 label: '👆☀️ 手動開操作時太陽高度頻度', 

1960 data: histPercent, 

1961 backgroundColor: 'rgba(255, 99, 132, 0.7)', 

1962 borderColor: 'rgba(255, 99, 132, 1)', 

1963 borderWidth: 1 

1964 }] 

1965 }, 

1966 options: { 

1967 responsive: true, 

1968 maintainAspectRatio: false, 

1969 scales: { 

1970 y: { 

1971 beginAtZero: true, 

1972 max: 100, 

1973 title: { 

1974 display: true, 

1975 text: '頻度(%)' 

1976 }, 

1977 ticks: { 

1978 callback: function(value) { 

1979 return value + '%'; 

1980 } 

1981 } 

1982 }, 

1983 x: { 

1984 title: { 

1985 display: true, 

1986 text: '太陽高度(度)' 

1987 } 

1988 } 

1989 } 

1990 } 

1991 }); 

1992 } 

1993 

1994 // 手動閉操作時太陽高度チャート 

1995 const manualCloseAltitudeCtx = document.getElementById('manualCloseAltitudeChart'); 

1996 if (manualCloseAltitudeCtx && chartData.manual_sensor_data && 

1997 chartData.manual_sensor_data.close_altitude && 

1998 chartData.manual_sensor_data.close_altitude.length > 0) { 

1999 const minAlt = Math.min(...chartData.manual_sensor_data.close_altitude); 

2000 const maxAlt = Math.max(...chartData.manual_sensor_data.close_altitude); 

2001 const bins = Array.from({length: 21}, (_, i) => minAlt + (maxAlt - minAlt) * i / 20); 

2002 const hist = createHistogram(chartData.manual_sensor_data.close_altitude, bins); 

2003 const total = chartData.manual_sensor_data.close_altitude.length; 

2004 const histPercent = hist.map(count => total > 0 ? (count / total) * 100 : 0); 

2005 

2006 new Chart(manualCloseAltitudeCtx, { 

2007 type: 'bar', 

2008 data: { 

2009 labels: bins.slice(0, -1).map(b => Math.round(b * 10) / 10), 

2010 datasets: [{ 

2011 label: '👆🌙 手動閉操作時太陽高度頻度', 

2012 data: histPercent, 

2013 backgroundColor: 'rgba(54, 162, 235, 0.7)', 

2014 borderColor: 'rgba(54, 162, 235, 1)', 

2015 borderWidth: 1 

2016 }] 

2017 }, 

2018 options: { 

2019 responsive: true, 

2020 maintainAspectRatio: false, 

2021 scales: { 

2022 y: { 

2023 beginAtZero: true, 

2024 max: 100, 

2025 title: { 

2026 display: true, 

2027 text: '頻度(%)' 

2028 }, 

2029 ticks: { 

2030 callback: function(value) { 

2031 return value + '%'; 

2032 } 

2033 } 

2034 }, 

2035 x: { 

2036 title: { 

2037 display: true, 

2038 text: '太陽高度(度)' 

2039 } 

2040 } 

2041 } 

2042 } 

2043 }); 

2044 } 

2045 } 

2046 """