ncepst-pyexceltools 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ModifyChart.py ADDED
@@ -0,0 +1,959 @@
1
+ # ModifyChart.py
2
+ # Copyright (c) 2025 ncepst
3
+ # SPDX-License-Identifier: MIT
4
+ """
5
+ 既存のExcelグラフ(散布図)の書式設定を行うモジュールです
6
+
7
+ 使用法:
8
+ from ModifyChart import ModifyChart, RGB
9
+ # chart に対して標準プリセット "std" の書式設定を適用、系列数=2 を指定
10
+ ModifyChart(chart, preset="std", NS=2)
11
+ """
12
+ from xlwings.constants import AxisType
13
+ from win32com.client import constants
14
+ # 型ヒントには Python 3.10 以降の記法を使用しています
15
+
16
+ # RGB値をExcel用の整数に変換するヘルパー関数
17
+ def RGB(r, g, b):
18
+ return r | (g << 8) | (b << 16)
19
+
20
+ # cm → pt 換算関数 (1 point = 1/72 inch, 1 inch = 2.54 cm)
21
+ def cm_to_pt(cm):
22
+ return cm * 72 / 2.54
23
+
24
+ def emphasize_line(axis,value=0,weight=1):
25
+ axis.CrossesAt = value
26
+ line = axis.Format.Line
27
+ line.Weight = weight
28
+ line.ForeColor.RGB = RGB(0,0,0)
29
+
30
+ PRESET = {
31
+ # "Aptos Narrow 本文"は Excel 2021以降のみ
32
+ "excel2021": {
33
+ "title_font_size": 14,
34
+ "title_font_color": RGB(89,89,89),
35
+ "title_font_bold": False,
36
+ "title_font_name": "Aptos Narrow 本文",
37
+ "axis_title_font_size": 10,
38
+ "axis_title_font_name": "Aptos Narrow 本文",
39
+ "axis_title_font_color": RGB(89,89,89),
40
+ "axis_title_font_bold": False,
41
+ "axis_tick_font_color": RGB(89, 89, 89),
42
+ "axis_tick_font_size": 9,
43
+ "axis_tick_font_name": "Aptos Narrow 本文",
44
+ "axis_tick_font_bold": False,
45
+ "axis_line": True,
46
+ "axis_line_color": RGB(191, 191, 191),
47
+ "axis_line_weight": 0.75,
48
+ "x_major_grid": True,
49
+ "y_major_grid": True,
50
+ "x_minor_grid": None,
51
+ "y_minor_grid": None,
52
+ "major_grid_color": RGB(217, 217, 217),
53
+ "major_grid_weight": 0.75,
54
+ # TickMark: None, Inside, Outside, Cross
55
+ "x_major_tickmark": constants.xlTickMarkNone, # 目盛の内向き/外向きなし
56
+ "y_major_tickmark": constants.xlTickMarkNone,
57
+ "x_minor_tickmark": None,
58
+ "y_minor_tickmark": None,
59
+ # 外枠の有無は引数で指定してください
60
+ "frame_color": RGB(217,217,217),
61
+ "frame_weight": 0.75,
62
+ "plot_area_frame": None,
63
+ "plot_area_frame_color": RGB(0,0,0),
64
+ "plot_area_frame_weight": 1.0,
65
+ "style": "line+marker",
66
+ "smooth": True,
67
+ "alpha": None,
68
+ "line_weight": 1.5,
69
+ "marker": "C",
70
+ "markersize": 5,
71
+ "y2_major_grid": False, # 副軸グリッド表示なし
72
+ "y2_minor_grid": False,
73
+ "y2_major_tickmark": constants.xlTickMarkNone,
74
+ "y2_minor_tickmark": None,
75
+ },
76
+ "std": {
77
+ "title_font_color": RGB(0,0,0),
78
+ "axis_title_font_color": RGB(0,0,0),
79
+ "axis_tick_font_color": RGB(0,0,0),
80
+ },
81
+ "no_grid":{
82
+ "x_major_grid": False,
83
+ "y_major_grid": False,
84
+ "plot_area_frame": True,
85
+ "axis_line": True,
86
+ "axis_line_color": RGB(0, 0, 0),
87
+ "x_major_tickmark": constants.xlTickMarkInside,
88
+ "y_major_tickmark": constants.xlTickMarkInside,
89
+ },
90
+ "grid":{
91
+ "title_font_name": "Arial",
92
+ "axis_title_font_name": "Arial",
93
+ "axis_tick_font_name": "Arial",
94
+ "title_font_bold": True,
95
+ "axis_title_font_bold": True,
96
+ "axis_tick_font_bold": True,
97
+ "major_grid_color": RGB(0, 0, 0),
98
+ "axis_line_color": RGB(0, 0, 0),
99
+ "x_major_tickmark": constants.xlTickMarkOutside,
100
+ "y_major_tickmark": constants.xlTickMarkOutside,
101
+ "frame_weight": 1,
102
+ "major_grid_weight": 1,
103
+ "axis_line_weight": 1,
104
+ "axis_title_font_size": 12,
105
+ "axis_tick_font_size": 12,
106
+ "title_font_size": 14,
107
+ },
108
+ "no_change":{
109
+ },
110
+ }
111
+ # std を excel2021 ベースで上書き
112
+ PRESET["std"] = {**PRESET["excel2021"], **PRESET["std"]}
113
+ # std ベースで上書き
114
+ PRESET["no_grid"] = {**PRESET["std"], **PRESET["no_grid"]}
115
+ PRESET["grid"] = {**PRESET["std"], **PRESET["grid"]}
116
+
117
+ marker_map = {
118
+ "C":constants.xlMarkerStyleCircle,
119
+ "S":constants.xlMarkerStyleSquare,
120
+ "D":constants.xlMarkerStyleDiamond,
121
+ "T":constants.xlMarkerStyleTriangle,
122
+ "N":constants.xlMarkerStyleNone
123
+ }
124
+
125
+ # False:無効化, None: 変更なし もしくは デフォルト値
126
+ # 優先順位 series cfg > 引数 > preset
127
+ def ModifyChart(chart, # ExcelのChartオブジェクト
128
+ ws = None, # None (系列ごとにRange指定する場合のみWorksheetオブジェクトを指定)
129
+ preset = "std", # プリセットスタイル名
130
+ width_cm = None, # グラフ幅(cm), Noneで変更なし
131
+ height_cm = None, # グラフ高さ(cm), Noneで変更なし
132
+ name = None, # グラフ名変更, Noneで変更なし
133
+ title: str|bool|None = None, # タイトル文字列, Falseで無効化, Noneで変更なし
134
+ title_font_color = None, # タイトルフォント色(RGB)
135
+ title_font_size = None, # タイトルフォントサイズ
136
+ title_space = +0, # タイトルとグラフの間隔(pt)
137
+ NS = 1, # データ系列数
138
+ series_list: list[dict]|None = None, # 各系列の設定
139
+ style = None, # 線+マーカーのスタイル
140
+ smooth = None, # Trueで曲線、Falseで折れ線
141
+ marker = None, # マーカー種類: "C":●, "S":■, "D":◆, "T":▲, "N":なし
142
+ alpha = None, # 線の透明度(0~1)
143
+ x_title: str|bool|None = None,# X軸タイトル文字列。Falseで無効化、Noneで変更なし
144
+ x_title_space = +0, # プロットエリアを下側に広げる場合はマイナス
145
+ x_min: float|str|None = None, # X軸最小値, "auto"で自動調整, Noneで変更なし
146
+ x_max: float|str|None = None, # X軸最大値, "auto"で自動調整, Noneで変更なし
147
+ x_major: float|None = None, # X軸主目盛間隔, Noneで変更なし, グリッド線表示設定はPRESET
148
+ x_minor: float|None = None, # X軸副目盛間隔, Noneで変更なし, グリッド線表示設定はPRESET
149
+ x_cross = None, # Y軸との交差位置, Noneで変更なし
150
+ x_format = None, # X軸表示形式 ("0.00", "0.0E+00", "0%" など), Noneで変更なし
151
+ x_log: bool|None = None, # Trueで対数表示, Noneで変更なし
152
+ y_title: str|bool|None = None,# Y軸タイトル, Noneで変更なし
153
+ y_title_space = +0,
154
+ y_min: float|str|None = None,
155
+ y_max: float|str|None = None,
156
+ y_major: float|None = None,
157
+ y_minor: float|None = None,
158
+ y_cross = None, # X軸との交差位置, Noneで変更なし
159
+ y_format = None,
160
+ y_log: bool|None = None,
161
+ y2_title: str|bool|None = None, # Y軸タイトル文字列, Falseで無効化, Noneで変更なし
162
+ y2_min: float|str|None = None,
163
+ y2_max: float|str|None = None,
164
+ y2_major: float|None = None,
165
+ y2_minor: float|None = None,
166
+ y2_format = None,
167
+ y2_log: bool|None = None,
168
+ frame_color: bool|int|None = None, # グラフ枠色 (False:枠なし, 0:黒枠, Noneでpreset)
169
+ legend: bool|str|None = None, # 凡例表示(False:非表示, Noneで変更なし, "right"で右側に表示)
170
+ legend_font_size = None, # 凡例のフォントサイズ, Noneで変更なし
171
+ legend_width_inc = 0, # 凡例ボックスの幅増減(pt)
172
+ legend_height_inc = 0, # 凡例ボックスの高さ増減(pt)
173
+ legend_right_space = 0, # 凡例の右端 = プロットエリアの右端を基準とした凡例位置制御
174
+ legent_top_space = 0, # 凡例の上端 = プロットエリアの上端を基準とした凡例位置制御
175
+ transparent_bg: bool|None = None, # 背景を透明化する場合はTrue
176
+ chart_type = None, # "bar"で棒グラフに変更
177
+ x_bold_line: float|None = None, # x_bold_line=0でx=0が太線
178
+ y_bold_line: float|None = None, # y_bold_line=0でy=0が太線
179
+ plot_area_space: str = "relative", # プロットエリア調整時の基準位置を"absolute"/"relative"で指定
180
+ width_inc = 0, # プロットエリアの幅増減(pt)
181
+ height_inc = 0, # プロットエリアの高さ増減(pt)
182
+ right_space = 0, # plot_area_space="abs"のときの右のスペース指定
183
+ ):
184
+
185
+ p = PRESET.get(preset, PRESET["std"]) or {}
186
+
187
+ # 注意) series_listの系列数をデータ範囲以下にしないと例外発生となる
188
+ # list / dict はミュータブルのため、デフォルト引数は None
189
+ if series_list is None:
190
+ series_list = [{"color": "blue"}]
191
+
192
+ COLOR_NAME_TO_RGB = {
193
+ "blue": RGB(68,114,196),
194
+ "orange":RGB(237,125,49),
195
+ "green":RGB(112,173,71),
196
+ "yellow":RGB(255,192,0),
197
+ "purple":RGB(112,48,160),
198
+ "brown":RGB(192,0,0),
199
+ "navy":RGB(0,32,96),
200
+ "gray":RGB(165,165,165),
201
+ "teal":RGB(0,128,128),
202
+ "cyan": RGB(0,255,255),
203
+ "magenta": RGB(255,0,255),
204
+ }
205
+ for cfg in series_list:
206
+ c = cfg.get("color")
207
+ if isinstance(c, str):
208
+ cfg["color"] = COLOR_NAME_TO_RGB.get(c.lower(), None)
209
+
210
+ # グラフ全体のサイズ変更
211
+ if width_cm not in (None, "", 0):
212
+ chart.width = cm_to_pt(width_cm)
213
+ if height_cm not in (None, "", 0):
214
+ chart.height = cm_to_pt(height_cm)
215
+
216
+ # グラフのチャート名 (エクセル画面左上の表示で確認できる)
217
+ if name not in (None, ""):
218
+ chart.name = name
219
+
220
+ # Chart本体 → api[1]
221
+ ch = chart.api[1]
222
+
223
+ # グラフタイトル
224
+ if title is False:
225
+ ch.HasTitle = True
226
+ ch.ChartTitle.Text = ""
227
+ elif title is True:
228
+ ch.HasTitle = True
229
+ ch.ChartTitle.Text = "グラフタイトル"
230
+ elif title not in (None, ""):
231
+ ch.HasTitle = True
232
+ ch.ChartTitle.Text = title
233
+
234
+ # 横軸のオプション
235
+ x_axis = ch.Axes(AxisType.xlCategory)
236
+ if x_min == "auto":
237
+ x_axis.MinimumScaleIsAuto = True
238
+ elif x_min not in (None, ""):
239
+ x_axis.MinimumScale = x_min # 最小値
240
+ if x_max == "auto":
241
+ x_axis.MaximumScaleIsAuto = True
242
+ elif x_max not in (None, ""):
243
+ x_axis.MaximumScale = x_max # 最大値
244
+ if p.get("x_major_grid") is not None:
245
+ x_axis.HasMajorGridlines = p.get("x_major_grid")
246
+ if p.get("x_major_tickmark") is not None:
247
+ x_axis.MajorTickMark = p.get("x_major_tickmark")
248
+ if p.get("x_minor_grid") is not None:
249
+ x_axis.HasMinorGridlines = p.get("x_minor_grid")
250
+ if p.get("x_minor_tickmark") is not None:
251
+ x_axis.MinorTickMark = p.get("x_minor_tickmark")
252
+ if x_major not in (None, ""):
253
+ x_axis.MajorUnit = x_major # 目盛間隔
254
+ if x_minor not in (None, ""):
255
+ x_axis.MinorUnit = x_minor
256
+ if x_cross not in (None, ""): # 交差位置(縦軸との交点)
257
+ x_axis.CrossesAt = x_cross
258
+ if x_format not in (None, ""):
259
+ x_axis.TickLabels.NumberFormatLocal = x_format
260
+ if x_log not in (None, ""):
261
+ x_axis.ScaleType = 1 if x_log == True else 0
262
+
263
+ # 横軸のタイトル
264
+ if x_title is False:
265
+ x_axis.HasTitle = False
266
+ elif x_title is True:
267
+ x_axis.HasTitle = True
268
+ x_axis.AxisTitle.Text = "x軸タイトル"
269
+ elif x_title not in (None, ""):
270
+ x_axis.HasTitle = True
271
+ x_axis.AxisTitle.Text = x_title
272
+
273
+ # 縦軸のオプション
274
+ y_axis = ch.Axes(AxisType.xlValue)
275
+ if y_min == "auto":
276
+ y_axis.MinimumScaleIsAuto = True
277
+ elif y_min not in (None, ""):
278
+ y_axis.MinimumScale = y_min
279
+ if y_max == "auto":
280
+ y_axis.MaximumScaleIsAuto = True
281
+ elif y_max not in (None, ""):
282
+ y_axis.MaximumScale = y_max
283
+ if p.get("y_major_grid") is not None:
284
+ y_axis.HasMajorGridlines = p.get("y_major_grid")
285
+ if p.get("y_major_tickmark") is not None:
286
+ y_axis.MajorTickMark = p.get("y_major_tickmark")
287
+ if p.get("y_minor_grid") is not None:
288
+ y_axis.HasMinorGridlines = p.get("y_minor_grid")
289
+ if p.get("y_minor_tickmark") is not None:
290
+ y_axis.MinorTickMark = p.get("y_minor_tickmark")
291
+ if y_major not in (None, ""):
292
+ y_axis.MajorUnit = y_major # 目盛間隔
293
+ if y_minor not in (None, ""):
294
+ y_axis.MinorUnit = y_minor
295
+ if y_cross not in (None, ""): # 交差位置(縦軸との交点)
296
+ y_axis.CrossesAt = y_cross
297
+ if y_format not in (None, ""):
298
+ y_axis.TickLabels.NumberFormatLocal = y_format
299
+ if y_log not in (None, ""):
300
+ y_axis.ScaleType = 1 if y_log == True else 0
301
+
302
+ # 縦軸のタイトル
303
+ if y_title is False:
304
+ y_axis.HasTitle = False
305
+ elif y_title is True:
306
+ y_axis.HasTitle = True
307
+ y_axis.AxisTitle.Text = "y軸タイトル"
308
+ elif y_title not in (None, ""):
309
+ y_axis.HasTitle = True
310
+ y_axis.AxisTitle.Text = y_title
311
+
312
+ if chart_type not in (None,""):
313
+ if chart_type == "bar":
314
+ chart_type = "column_clustered"
315
+ chart.chart_type = chart_type
316
+
317
+ # 系列の設定 -----------------------------------------------------------------------------
318
+ use_secondary = False
319
+ NS = max(len(series_list), NS)
320
+ for i in range(1, NS + 1):
321
+ if i <= len(series_list):
322
+ cfg = series_list[i - 1]
323
+ else:
324
+ cfg = {}
325
+ try:
326
+ series = ch.SeriesCollection(i)
327
+ except Exception as e:
328
+ print(f"系列{i}を取得できません: {e}")
329
+ continue
330
+
331
+ # 系列名
332
+ if cfg.get("name") not in (None, ""):
333
+ series.Name = cfg["name"]
334
+
335
+ # XValues / Values
336
+ if cfg.get("XValues") not in (None, ""):
337
+ try:
338
+ wsx = cfg.get("sheet",ws)
339
+ if isinstance(wsx, str):
340
+ wsx = ws.parent.sheets[wsx]
341
+ series.XValues = wsx.range(cfg["XValues"]).api
342
+ except Exception:
343
+ print(f"系列{i}: wsの設定または範囲指定に問題")
344
+ if cfg.get("Values") not in (None, ""):
345
+ try:
346
+ wsy = cfg.get("sheet",ws)
347
+ if isinstance(wsy, str):
348
+ wsy = ws.parent.sheets[wsy]
349
+ series.Values = wsy.range(cfg["Values"]).api
350
+ except Exception:
351
+ print(f"系列{i}: wsの設定または範囲指定に問題")
352
+
353
+ try:
354
+ if cfg.get("chart_type") == "bar":
355
+ series.ChartType = constants.xlColumnClustered
356
+
357
+ # 色
358
+ color = cfg.get("color")
359
+ if color not in (None, ""):
360
+ try:
361
+ series.Format.Line.ForeColor.RGB = color # 線の色
362
+ series.MarkerForegroundColor = color # マーカー枠線の色
363
+ series.MarkerBackgroundColor = color # マーカー内部の色
364
+ except:
365
+ # hasattr(series.Format, "Fill")
366
+ series.Format.Fill.ForeColor.RGB = color
367
+
368
+ # スムーズ
369
+ if cfg.get("smooth") not in (None, ""):
370
+ smooth_i = cfg["smooth"]
371
+ elif smooth not in (None, ""):
372
+ smooth_i = smooth
373
+ else:
374
+ smooth_i = p.get("smooth",True)
375
+ if smooth_i not in (None, ""):
376
+ try:
377
+ series.Smooth = bool(smooth_i)
378
+ except:
379
+ pass
380
+
381
+ # スタイル(線とマーカー)
382
+ if cfg.get("style") not in (None, ""):
383
+ style_i = cfg["style"]
384
+ elif style not in (None, ""):
385
+ style_i = style
386
+ else:
387
+ style_i = p.get("style") or None
388
+
389
+ if style_i is None:
390
+ pass
391
+ elif "marker" in style_i:
392
+ if cfg.get("marker") not in (None, ""):
393
+ marker_i = cfg["marker"]
394
+ elif marker not in (None, ""):
395
+ marker_i = marker
396
+ elif p.get("marker") not in (None, ""):
397
+ marker_i = p["marker"]
398
+ else:
399
+ marker_i = None
400
+ series.MarkerStyle = marker_map.get(marker_i, constants.xlMarkerStyleCircle) # マーカー:〇
401
+ series.MarkerSize = cfg.get("size",p.get("markersize")) # マーカーサイズ
402
+ else:
403
+ series.MarkerStyle = constants.xlMarkerStyleNone
404
+
405
+ if style_i is None:
406
+ pass
407
+ elif "line" in style_i:
408
+ series.Format.Line.Visible = True
409
+ series.Format.Line.Weight = cfg.get("weight", p.get("line_weight")) # 線の太さ(pt)
410
+ elif isinstance(style_i, str) and ("dash" in style_i or "--" in style_i):
411
+ series.Format.Line.Visible = True
412
+ series.Format.Line.DashStyle = 4
413
+ elif isinstance(style_i, str) and ("chain" in style_i or "-." in style_i):
414
+ series.Format.Line.Visible = True
415
+ series.Format.Line.DashStyle = 5
416
+ else:
417
+ series.Format.Line.Visible = False
418
+
419
+ # 線の透明度の設定(0~1), デフォルト値は0
420
+ if cfg.get("alpha") not in (None, ""):
421
+ alpha_i = cfg["alpha"]
422
+ elif alpha not in (None, ""):
423
+ alpha_i = alpha
424
+ elif p.get("alpha") not in (None, ""):
425
+ alpha_i = p["alpha"]
426
+ else:
427
+ alpha_i = None
428
+ if alpha_i not in (None, ""):
429
+ series.Format.Line.Transparency = float(alpha_i)
430
+
431
+ axis_name = cfg.get("axis", "primary")
432
+ if axis_name == "secondary" or axis_name == "y2":
433
+ series.AxisGroup = constants.xlSecondary
434
+ use_secondary = True
435
+ else:
436
+ series.AxisGroup = constants.xlPrimary
437
+
438
+ tl = cfg.get("trendline")
439
+ if tl not in (None, ""):
440
+ try:
441
+ series.Trendlines().Delete()
442
+ except Exception:
443
+ pass
444
+ trend = None
445
+ if tl == 0:
446
+ pass
447
+ elif isinstance(tl, int):
448
+ if tl == 1:
449
+ trend = series.Trendlines().Add(Type=constants.xlLinear)
450
+ elif 2 <= tl <= 6:
451
+ trend = series.Trendlines().Add(Type=constants.xlPolynomial)
452
+ trend.Order = tl
453
+ elif isinstance(tl, str):
454
+ tl_map = {
455
+ "exp": constants.xlExponential, # 指数
456
+ "log": constants.xlLogarithmic, # 対数
457
+ "pow": constants.xlPower, # 累乗
458
+ "mov": constants.xlMovingAvg, # 移動平均
459
+ }
460
+ ttype = tl_map.get(tl.lower())
461
+ if ttype is not None:
462
+ trend = series.Trendlines().Add(Type=ttype)
463
+ if trend is not None:
464
+ if cfg.get("trendline_name") is not None:
465
+ trend.Name = cfg.get("trendline_name", "Regression line")
466
+ trend.Format.Line.ForeColor.RGB = cfg.get("trendline_color",cfg.get("color",RGB(0,0,0)))
467
+ trend.Format.Line.Weight = cfg.get("trendline_weight", 1.5)
468
+ Dashstyle = cfg.get("trendline_style", "solid").lower()
469
+ if Dashstyle in ("dash", "--"):
470
+ trend.Format.Line.DashStyle = 4
471
+ elif Dashstyle in ("chain", "-."):
472
+ trend.Format.Line.DashStyle = 5
473
+ else: # solid / default
474
+ # trend.Format.Line.DashStyle = constants.xlSolid
475
+ pass
476
+ t_option = cfg.get("trendline_option", "")
477
+ if "eq" in t_option:
478
+ trend.DisplayEquation = True # 近似式を表示
479
+ # trend.DataLabel.Left
480
+ # trend.DataLabel.Top
481
+ if "r2" in t_option:
482
+ trend.DisplayRSquared = True # 決定係数(R²)を表示
483
+
484
+ if cfg.get("legend") is False:
485
+ series.HasLegendKey = False
486
+
487
+ if cfg.get("data_label", False):
488
+ series.HasDataLabels = True
489
+
490
+ except Exception as e:
491
+ print(f"系列{i}の設定でエラー発生:{e}")
492
+
493
+ # フォーマットの設定 -------------------------------------------------
494
+ try:
495
+ # タイトルの設定
496
+ if ch.HasTitle:
497
+ ch.ChartTitle.Format.TextFrame2.TextRange.Font.Bold = p.get("title_font_bold")
498
+ ch.ChartTitle.Format.TextFrame2.TextRange.Font.Name = p.get("title_font_name")
499
+ if title_font_color not in (None, ""):
500
+ ch.ChartTitle.Format.TextFrame2.TextRange.Font.Fill.ForeColor.RGB = title_font_color
501
+ else:
502
+ ch.ChartTitle.Format.TextFrame2.TextRange.Font.Fill.ForeColor.RGB = p.get("title_font_color")
503
+ if title_font_size not in (None, ""):
504
+ ch.ChartTitle.Format.TextFrame2.TextRange.Font.Size = title_font_size
505
+ else:
506
+ ch.ChartTitle.Format.TextFrame2.TextRange.Font.Size = p.get("title_font_size")
507
+ except Exception as e:
508
+ print("タイトルの設定でエラー:",e)
509
+
510
+ try:
511
+ # グリッド線の設定
512
+ axes = [x_axis, y_axis]
513
+ for ax in axes:
514
+ if ax.HasMajorGridlines:
515
+ ax.MajorGridlines.Format.Line.ForeColor.RGB = p.get("major_grid_color")
516
+ ax.MajorGridlines.Format.Line.Weight = p.get("major_grid_weight")
517
+ if p.get("axis_line") is None:
518
+ pass
519
+ elif p.get("axis_line") is True:
520
+ ax.Format.Line.Visible = True
521
+ ax.Format.Line.ForeColor.RGB = p.get("axis_line_color")
522
+ ax.Format.Line.Weight = p.get("axis_line_weight")
523
+ elif p.get("axis_line") is False:
524
+ ax.Format.Line.Visible = False
525
+ except Exception as e:
526
+ print("グリッド線の設定でエラー:",e)
527
+
528
+ try:
529
+ # 副軸の設定
530
+ if use_secondary:
531
+ y2 = ch.Axes(AxisType.xlValue, constants.xlSecondary)
532
+ axes.append(y2)
533
+ if y2_min == "auto":
534
+ y2.MinimumScaleIsAuto = True
535
+ elif y2_min not in (None, ""):
536
+ y2.MinimumScale = y2_min
537
+ if y2_max == "auto":
538
+ y2.MaximumScaleIsAuto = True
539
+ elif y2_max not in (None, ""):
540
+ y2.MaximumScale = y2_max
541
+ if p.get("y2_major_grid") is not None:
542
+ y2.HasMajorGridlines = p.get("y2_major_grid")
543
+ if p.get("y2_major_tickmark") is not None:
544
+ y2.MajorTickMark = p.get("y2_major_tickmark")
545
+ if p.get("y2_minor_grid") is not None:
546
+ y2.HasMinorGridlines = p.get("y2_minor_grid")
547
+ if p.get("y2_minor_tickmark") is not None:
548
+ y2.MinorTickMark = p.get("y2_minor_tickmark")
549
+ if y2_major not in (None, ""):
550
+ y2.MajorUnit = y2_major
551
+ if y2_minor not in (None, ""):
552
+ y2.MinorUnit = y2_minor
553
+ if y2_format not in (None, ""):
554
+ y2.TickLabels.NumberFormatLocal = y2_format
555
+ if y2_log not in (None, ""):
556
+ y2.ScaleType = 1 if y2_log == True else 0
557
+ if y2_title is False:
558
+ y2.HasTitle = False
559
+ elif y2_title is True:
560
+ y2.HasTitle = True
561
+ y2.AxisTitle.Text = "副軸タイトル"
562
+ elif y2_title not in (None, ""):
563
+ y2.HasTitle = True
564
+ y2.AxisTitle.Text = y2_title
565
+ except Exception as e:
566
+ print("副軸の設定でエラー:",e)
567
+
568
+ try:
569
+ # 軸のフォント設定
570
+ for ax in axes:
571
+ tl_font = ax.TickLabels.Font
572
+ tl_font.Color = p.get("axis_tick_font_color")
573
+ tl_font.Size = p.get("axis_tick_font_size")
574
+ tl_font.Name = p.get("axis_tick_font_name")
575
+ tl_font.Bold = p.get("axis_tick_font_bold")
576
+ if ax.HasTitle:
577
+ ax_font = ax.AxisTitle.Format.TextFrame2.TextRange.Font
578
+ ax_font.Fill.ForeColor.RGB = p.get("axis_title_font_color")
579
+ ax_font.Bold = p.get("axis_title_font_bold")
580
+ ax_font.Size = p.get("axis_title_font_size")
581
+ ax_font.Name = p.get("axis_title_font_name")
582
+ except Exception as e:
583
+ print("軸のフォント設定でエラー:",e)
584
+
585
+ try:
586
+ # chart_area/plot_area のオブジェクト取得
587
+ chart_area = ch.ChartArea
588
+ plot_area = ch.PlotArea
589
+
590
+ # 外枠の設定
591
+ if frame_color is False: # False:枠なし、0:黒枠
592
+ chart_area.Border.LineStyle = 0 # 枠なし
593
+ elif frame_color not in (None, True, ""):
594
+ chart_area.Format.Line.ForeColor.RGB = frame_color
595
+ chart_area.Format.Line.Weight = p.get("frame_weight")
596
+ else:
597
+ chart_area.Format.Line.ForeColor.RGB = p.get("frame_color") # 薄いグレー
598
+ chart_area.Format.Line.Weight = p.get("frame_weight") # 枠線の太さ(pt)
599
+
600
+ # プロットエリアの枠設定
601
+ if p.get("plot_area_frame") is not None:
602
+ plot_area.Format.Line.Visible = p.get("plot_area_frame")
603
+ if p.get("plot_area_frame") is True:
604
+ plot_area.Format.Line.ForeColor.RGB = p.get("plot_area_frame_color")
605
+ plot_area.Format.Line.Weight = p.get("plot_area_frame_weight")
606
+
607
+ # 背景の透明化設定
608
+ if transparent_bg is True:
609
+ chart_area.Format.Fill.Visible = False
610
+ plot_area.Format.Fill.Visible = False
611
+ elif transparent_bg is False:
612
+ chart_area.Format.Fill.Visible = True
613
+ plot_area.Format.Fill.Visible = True
614
+ chart_area.Format.Fill.ForeColor.RGB = RGB(255,255,255)
615
+ plot_area.Format.Fill.ForeColor.RGB = RGB(255,255,255)
616
+
617
+ # ラインの強調
618
+ if x_bold_line is not None:
619
+ emphasize_line(x_axis,x_bold_line)
620
+ if y_bold_line is not None:
621
+ emphasize_line(y_axis,y_bold_line)
622
+ except Exception as e:
623
+ print("チャートエリア、プロットエリアの設定でエラー:",e)
624
+
625
+ try:
626
+ # 凡例を一度無効にする(例外あり)
627
+ if isinstance(legend, str) and "right" in legend:
628
+ ch.HasLegend = True
629
+ if legend_font_size not in (None, ""):
630
+ ch.Legend.Format.TextFrame2.TextRange.Font.Size = legend_font_size
631
+ elif legend not in (None, ""):
632
+ ch.HasLegend = False
633
+
634
+ # プロットエリアの調整
635
+ debug = False
636
+ if plot_area_space in ("absolute","abs"):
637
+
638
+ def fit_inside(chart, L=0, T=0, R=0, B=0, it=6, tol=0.8):
639
+ ch = chart.api[1]
640
+ co, ca, pl = ch.Parent, ch.ChartArea, ch.PlotArea
641
+ pl.Position = constants.xlChartElementPositionCustom
642
+
643
+ tgtL = float(co.Left) + float(L)
644
+ tgtT = float(co.Top) + float(T)
645
+ tgtR = float(co.Left) + float(co.Width) - float(R)
646
+ tgtB = float(co.Top) + float(co.Height) - float(B)
647
+
648
+ for _ in range(it):
649
+ # 現在の Inside(シート基準)を読む
650
+ curL = float(ca.Left) + float(pl.InsideLeft)
651
+ curT = float(ca.Top) + float(pl.InsideTop)
652
+ curR = curL + float(pl.InsideWidth)
653
+ curB = curT + float(pl.InsideHeight)
654
+
655
+ # --- 左(右端固定で合わせる)---
656
+ dL = tgtL - curL
657
+ if abs(dL) > tol:
658
+ # 右端(外形の Right)を維持するように、Left を動かし Width を逆側に補正
659
+ right_outer = float(pl.Left) + float(pl.Width)
660
+ new_left = float(pl.Left) + dL
661
+ new_width = max(right_outer - new_left, 10.0)
662
+ pl.Left, pl.Width = new_left, new_width
663
+
664
+ # Inside 再読込(安定させるため)
665
+ curL = float(ca.Left) + float(pl.InsideLeft)
666
+
667
+ # --- 右(Left 固定で合わせる)---
668
+ curR = float(ca.Left) + float(pl.InsideLeft) + float(pl.InsideWidth)
669
+ dR = tgtR - curR
670
+ if abs(dR) > tol:
671
+ # Left 外形は固定、Width だけ増減
672
+ pl.Width = max(float(pl.Width) + dR, 10.0)
673
+
674
+ # --- 上(下端固定で合わせる)---
675
+ curT = float(ca.Top) + float(pl.InsideTop)
676
+ dT = tgtT - curT
677
+ if abs(dT) > tol:
678
+ bottom_outer = float(pl.Top) + float(pl.Height)
679
+ new_top = float(pl.Top) + dT
680
+ new_height = max(bottom_outer - new_top, 10.0)
681
+ pl.Top, pl.Height = new_top, new_height
682
+
683
+ # --- 下(Top 固定で合わせる)---
684
+ curB = float(ca.Top) + float(pl.InsideTop) + float(pl.InsideHeight)
685
+ dB = tgtB - curB
686
+ if abs(dB) > tol:
687
+ pl.Height = max(float(pl.Height) + dB, 10.0)
688
+
689
+ if debug:
690
+ print(
691
+ f"PlotArea\n"
692
+ f"Outer (sheet): "
693
+ f"Left={pl.Left:.1f}, Top={pl.Top:.1f}, "
694
+ f"Width={pl.Width:.1f}, Height={pl.Height:.1f} \n"
695
+ f"Inside (chart): "
696
+ f"Left={pl.InsideLeft:.1f}, Top={pl.InsideTop:.1f}, "
697
+ f"Width={pl.InsideWidth:.1f}, Height={pl.InsideHeight:.1f}"
698
+ )
699
+
700
+ fit_inside(chart, L=y_title_space+42, T=title_space+35, R=right_space+20, B=x_title_space+40)
701
+
702
+ else: # relative / dafault
703
+ left = plot_area.InsideLeft + y_title_space
704
+ top = plot_area.InsideTop + title_space
705
+ width = plot_area.InsideWidth - y_title_space + width_inc
706
+ height = plot_area.InsideHeight - title_space - x_title_space + height_inc
707
+
708
+ if debug:
709
+ print(
710
+ f"PlotArea\n"
711
+ f"Inside (chart): "
712
+ f"Left={left:.1f}, Top={top:.1f}, "
713
+ f"Width={width:.1f}, Height={height:.1f}"
714
+ )
715
+
716
+ plot_area.InsideLeft = left
717
+ plot_area.InsideTop = top
718
+ plot_area.InsideWidth = width
719
+ plot_area.InsideHeight = height
720
+
721
+ # 凡例設定
722
+ if legend in (None, ""):
723
+ pass
724
+ elif legend is False:
725
+ ch.HasLegend = False
726
+ else:
727
+ ch.HasLegend = True
728
+ if legend_font_size not in (None, ""):
729
+ ch.Legend.Format.TextFrame2.TextRange.Font.Size = legend_font_size
730
+ if legend_width_inc!=0:
731
+ ch.Legend.Width += legend_width_inc
732
+ if legend_height_inc!=0:
733
+ ch.Legend.Height += legend_height_inc
734
+ if legend is True:
735
+ pass
736
+ else:
737
+ if "T" in legend: # 大文字のTを含む場合
738
+ ch.Legend.Top = plot_area.InsideTop + legent_top_space
739
+ elif "B" in legend: # 大文字のBを含む場合
740
+ ch.Legend.Top = plot_area.InsideTop + plot_area.InsideHeight - ch.Legend.Height - legent_top_space
741
+ if "R" in legend: # 大文字のRを含む場合
742
+ ch.Legend.Left = plot_area.InsideLeft + plot_area.InsideWidth - ch.Legend.Width - legend_right_space
743
+ elif "L" in legend: # 大文字のLを含む場合
744
+ ch.Legend.Left = plot_area.InsideLeft + legend_right_space
745
+ elif "C" in legend: # 大文字のCを含む場合
746
+ ch.Legend.Left = plot_area.InsideLeft + (plot_area.InsideWidth - ch.Legend.Width)/2
747
+ if "bw" in legend: # background white
748
+ ch.Legend.Format.Fill.ForeColor.RGB = RGB(255, 255, 255)
749
+ ch.Legend.Format.Fill.Visible = True
750
+ if "fb" in legend: # frame black
751
+ ch.Legend.Format.Line.ForeColor.RGB = RGB(0, 0, 0)
752
+ ch.Legend.Format.Line.Weight = 0.75
753
+ ch.Legend.Format.Line.Visible = True
754
+ if "tb" in legend: # text black
755
+ ch.Legend.Format.TextFrame2.TextRange.Font.Fill.ForeColor.RGB = RGB(0, 0, 0)
756
+ except Exception as e:
757
+ print("凡例 もしくは プロットエリアの調整でエラー:",e)
758
+
759
+ return chart
760
+
761
+ # プロットエリア内に帯や注釈用のボックスを配置する
762
+ def add_shape(chart, x_start=None, x_end=None, y_start=None, y_end=None,
763
+ left=None, width=None, top=None, height=None, delta_width=0, delta_height=0,
764
+ color=RGB(0,0,255), alpha=0.8, frame_color=None, white_box=None,
765
+ text=None, font_color=RGB(0,0,0), font_name="Calibri", font_size=10,
766
+ font_bold=True, alignment=None, auto_size=None,
767
+ right=None, bottom=None):
768
+ """
769
+ 優先順位 帯の指定(x_start, x_end) > 位置の指定(left, right) の両者指定 > left or right と width
770
+ (y_start, y_end) > 位置の指定(top, bottom) の両者指定 > top or bottom と height
771
+ 1) start, x_end, y_start, y_endをすべてNoneとすることで位置 (数値pt または "数値%") で指定可能です。
772
+ 2) left, right, top, bottomには 負の数値、"負の数値%" を指定できます。width,heightは0より大きい値を指定してください。
773
+ 3) left + right > width もしくは top + bottom > height では負のサイズにより、エラー発生します。
774
+ 4) delta_width, delta_heightで Excel描画都合の微調整ができます。
775
+ 5) white_box = True では、不透明の白背景、黒枠のテキストボックスとなります。それ以外の条件はNoneにしてください。
776
+ 6) white_boxのNone/True に関わらず、text = "1行目\n2行目" (改行\n) でテキスト文字を指定できます。
777
+ 7) テキストありで、auto_size=True の場合、left\topを基準に右下方向へ自動拡張されます。
778
+ 8) alignment ではテキスト配置を 左揃え:"left"、中央揃え:"center"、右揃え:"right"
779
+ と 上揃え:"top", 上下中央揃え:"middle", 下揃え:"bottom を組み合わせて指定できます。
780
+ """
781
+ ch = chart.api[1]
782
+ plot_area = ch.PlotArea
783
+ x_axis = ch.Axes(constants.xlCategory)
784
+ x_min, x_max = x_axis.MinimumScale, x_axis.MaximumScale
785
+ y_axis = ch.Axes(constants.xlValue)
786
+ y_min, y_max = y_axis.MinimumScale, y_axis.MaximumScale
787
+
788
+ # (x_start=None, x_end=None) or (y_start=None, y_end=None) で使用する関数
789
+ def parse_size(value, total_size):
790
+ """
791
+ value:
792
+ - 数値 → px
793
+ - "xx%" → total_size * xx/100
794
+ - None → 呼び出し側で解釈
795
+ """
796
+ if isinstance(value, str) and value.endswith("%"):
797
+ try:
798
+ pct = float(value.strip("%")) / 100
799
+ return total_size * pct
800
+ except ValueError:
801
+ raise ValueError(f"Invalid percentage value: {value}")
802
+ else:
803
+ return value
804
+
805
+ def resolve_1d(start, end, axis_min, axis_max, left, right, width, origin, total):
806
+ if start is not None and end is None: end = axis_max
807
+ if start is None and end is not None: start = axis_min
808
+ if start is not None and end is not None:
809
+ left = origin + (start - axis_min) / (axis_max - axis_min) * total
810
+ width = (end - start) / (axis_max - axis_min) * total
811
+ elif left is None and right is None:
812
+ left = origin
813
+ width = parse_size(width, total)
814
+ if width is None:
815
+ width = total
816
+ elif left is not None and right is not None:
817
+ left_val = parse_size(left, total)
818
+ right_val = parse_size(right, total)
819
+ left = origin + left_val
820
+ width = total - left_val - right_val
821
+ elif left is not None and right is None:
822
+ left_val = parse_size(left, total)
823
+ left = origin + left_val
824
+ width = parse_size(width, total)
825
+ if width is None:
826
+ width = total - left_val
827
+ elif left is None and right is not None:
828
+ right_val = parse_size(right, total)
829
+ width = parse_size(width, total)
830
+ if width is None:
831
+ width = total - right_val
832
+ left = origin + total - width - right_val
833
+ return left, width
834
+
835
+ # X軸座標変換
836
+ origin = plot_area.InsideLeft
837
+ total = plot_area.InsideWidth
838
+ left, width = resolve_1d(x_start, x_end, x_min, x_max, left, right, width, origin, total)
839
+ # Excel描画の都合の微調整
840
+ width += delta_width
841
+ # Y軸座標変換
842
+ origin = plot_area.InsideTop
843
+ total = plot_area.InsideHeight
844
+ top, height = resolve_1d(y_start, y_end, y_min, y_max, top, bottom, height, origin, total)
845
+ # Excel描画の都合の微調整
846
+ height += delta_height
847
+
848
+ if width <= 0 or height <= 0:
849
+ raise ValueError(f"Invalid shape size: width={width}, height={height}")
850
+
851
+ if white_box:
852
+ shape = ch.Shapes.AddTextbox(1, left, top, width, height)
853
+ color = RGB(255,255,255)
854
+ alpha = 0
855
+ frame_color = RGB(0,0,0)
856
+ else:
857
+ # 1 = msoShapeRectangle
858
+ shape = ch.Shapes.AddShape(1, left, top, width, height)
859
+
860
+ shape.Fill.ForeColor.RGB = color
861
+ shape.Fill.Transparency = alpha
862
+ if frame_color is None:
863
+ shape.Line.Visible = False
864
+ else:
865
+ shape.Line.Visible = True
866
+ shape.Line.ForeColor.RGB = frame_color
867
+ shape.Line.Weight = 0.75
868
+ if text is not None:
869
+ shape.TextFrame.Characters().Text = text
870
+ if auto_size:
871
+ shape.TextFrame.AutoSize = True
872
+ shape.TextFrame2.TextRange.Font.Fill.ForeColor.RGB = font_color
873
+ shape.TextFrame2.TextRange.Font.Name = font_name
874
+ shape.TextFrame2.TextRange.Font.Size = font_size
875
+ shape.TextFrame2.TextRange.Font.Bold = font_bold
876
+ if alignment is not None:
877
+ alignment = alignment.strip().lower()
878
+ if "left" in alignment:
879
+ shape.TextFrame.HorizontalAlignment = constants.xlHAlignLeft
880
+ elif "center" in alignment:
881
+ shape.TextFrame.HorizontalAlignment = constants.xlHAlignCenter
882
+ elif "right" in alignment:
883
+ shape.TextFrame.HorizontalAlignment = constants.xlHAlignRight
884
+ if "top" in alignment:
885
+ shape.TextFrame.VerticalAlignment = constants.xlVAlignTop
886
+ elif "middle" in alignment:
887
+ shape.TextFrame.VerticalAlignment = constants.xlVAlignCenter
888
+ elif "bottom" in alignment:
889
+ shape.TextFrame.VerticalAlignment = constants.xlVAlignBottom
890
+ return shape
891
+
892
+ def add_line(chart, x=None, y=None, color=RGB(0, 0, 0), weight=1.5, dash=True):
893
+ """
894
+ プロットエリア内に基準線を描画する
895
+
896
+ x : 縦線(x軸の値)
897
+ y : 横線(y軸の値)
898
+ """
899
+
900
+ if (x is None) == (y is None):
901
+ raise ValueError("x または y のどちらか一方だけ指定してください")
902
+
903
+ ch = chart.api[1]
904
+ plot = ch.PlotArea
905
+
906
+ x_axis = ch.Axes(constants.xlCategory)
907
+ y_axis = ch.Axes(constants.xlValue)
908
+
909
+ # --- 縦線 ---
910
+ if x is not None:
911
+ x_pos = plot.InsideLeft + (x - x_axis.MinimumScale)/(x_axis.MaximumScale - x_axis.MinimumScale)*plot.InsideWidth
912
+ x1 = x2 = x_pos
913
+ y1 = plot.InsideTop
914
+ y2 = plot.InsideTop + plot.InsideHeight
915
+
916
+ # --- 横線 ---
917
+ else:
918
+ y_pos = plot.InsideTop + (1 - (y - y_axis.MinimumScale)/(y_axis.MaximumScale - y_axis.MinimumScale))*plot.InsideHeight
919
+ y1 = y2 = y_pos
920
+ x1 = plot.InsideLeft
921
+ x2 = plot.InsideLeft + plot.InsideWidth
922
+
923
+ line = ch.Shapes.AddLine(x1, y1, x2, y2)
924
+ line.Line.ForeColor.RGB = color
925
+ line.Line.Weight = weight
926
+ if dash:
927
+ line.Line.DashStyle = 4
928
+ return line
929
+
930
+ def template(chart,user,name="グラフ 1",
931
+ width_cm=None, height_cm=None, title=None, x_title=None, y_title=None):
932
+ """
933
+ グラフテンプレート機能を使う
934
+ user: ユーザー名
935
+ name: テンプレート名
936
+ """
937
+ if width_cm is not None:
938
+ chart.width = cm_to_pt(width_cm)
939
+ if height_cm is not None:
940
+ chart.height = cm_to_pt(height_cm)
941
+
942
+ ch = chart.api[1]
943
+ ch.ApplyChartTemplate(rf"C:\Users\{user}\AppData\Roaming\Microsoft\Templates\Charts\{name}.crtx")
944
+
945
+ if title is not None:
946
+ ch.HasTitle = True
947
+ ch.ChartTitle.Text = title
948
+
949
+ if x_title is not None:
950
+ x_axis = ch.Axes(AxisType.xlCategory)
951
+ x_axis.HasTitle = True
952
+ x_axis.AxisTitle.Text = x_title
953
+
954
+ if y_title is not None:
955
+ y_axis = ch.Axes(AxisType.xlValue)
956
+ y_axis.HasTitle = True
957
+
958
+ y_axis.AxisTitle.Text = y_title
959
+