Implement arc tick generation with 3 tiers across all scale mappings

This commit is contained in:
2026-07-28 23:49:27 +02:00
parent 3643b65b15
commit 962d2fdf11
+107
View File
@@ -344,6 +344,95 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma
label_size=5.1126, font_key=None):
fd = _FONTS.get(font_key) or _FONTS.get(_DEFAULT_FONT) or next(iter(_FONTS.values()))
if scale_type == 'log':
if min_val <= 0 or max_val <= 0:
raise ValueError("Logarithmic scale requires min and max values greater than 0.")
lmin, lmax = math.log(min_val), math.log(max_val)
lspan = lmax - lmin
if lspan == 0:
raise ValueError("Logarithmic scale requires min and max values to differ.")
def value_to_angle(v):
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (math.log(v) - lmin) / lspan
def major_value(i):
return min_val * (max_val / min_val) ** (i / big_ticks)
elif scale_type == 'sqrt':
if min_val < 0:
raise ValueError("Square-root scale requires min value ≥ 0.")
span = max_val - min_val
if span <= 0:
raise ValueError("Square-root scale requires max value greater than min.")
def value_to_angle(v):
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * math.sqrt((v - min_val) / span)
def major_value(i):
return min_val + span * (i / big_ticks) ** 2
elif scale_type == 'reciprocal':
if min_val <= 0:
raise ValueError("Reciprocal scale requires min and max values greater than 0.")
if not math.isinf(max_val) and max_val <= min_val:
raise ValueError("Reciprocal scale requires max value greater than min.")
if math.isinf(max_val):
def value_to_angle(v):
if math.isinf(v):
return ANGLE_MIN
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * min_val / v
def major_value(i):
if i == 0:
return math.inf
return min_val * big_ticks / i
else:
if max_val <= 0:
raise ValueError("Reciprocal scale requires min and max values greater than 0.")
k = 1.0 / min_val - 1.0 / max_val
def value_to_angle(v):
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (1.0 / v - 1.0 / max_val) / k
def major_value(i):
return 1.0 / (1.0 / max_val + i / big_ticks * k)
else:
span = max_val - min_val
def value_to_angle(v):
if span == 0:
return ANGLE_MIN
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (v - min_val) / span
def major_value(i):
return min_val + span * i / big_ticks
big_interval_angle = (ANGLE_MAX - ANGLE_MIN) / big_ticks
big_angles = [ANGLE_MIN + i * big_interval_angle for i in range(big_ticks + 1)]
big_vals = [major_value(i) for i in range(big_ticks + 1)]
if custom_labels:
label_count = max(1, min(len(custom_labels), big_ticks + 1))
custom_labels = custom_labels[:label_count]
else:
label_count = max(2, min(label_count, big_ticks + 1))
if label_count == 1:
label_indices = [0]
else:
label_indices = [round(k * big_ticks / (label_count - 1)) for k in range(label_count)]
def _tick_line(angle_deg, length, width):
x = PIVOT_X
y1 = PIVOT_Y - R0
y2 = PIVOT_Y - R0 - length
return (
f' <line x1="{x:.5f}" y1="{y1:.5f}" x2="{x:.5f}" y2="{y2:.5f}"'
f' style="stroke:#1a1a1a;stroke-width:{width:.5f};stroke-linecap:butt"'
f' transform="rotate({angle_deg:.5f},{PIVOT_X:.5f},{PIVOT_Y:.5f})" />'
)
font_face = (
'<defs><style>'
f'@font-face {{font-family:"{fd.label}";'
@@ -384,6 +473,24 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma
f' cx="{PIVOT_X:.5f}" cy="{PIVOT_Y:.5f}" r="{PIVOT_CIRCLE_R}" />'
)
for angle in big_angles:
parts.append(_tick_line(angle, MAJOR_LEN, MAJOR_W))
if small_ticks > 0:
sub = small_ticks + 1
medium_j = sub // 2 if small_ticks % 2 == 1 else None
for i in range(big_ticks):
for j in range(1, small_ticks + 1):
if scale_type == 'sqrt':
angle = big_angles[i] + (j / sub) ** 2 * (big_angles[i + 1] - big_angles[i])
elif scale_type == 'reciprocal' or math.isinf(big_vals[i]):
angle = big_angles[i] + (j / sub) * (big_angles[i + 1] - big_angles[i])
else:
v = big_vals[i] + j * (big_vals[i + 1] - big_vals[i]) / sub
angle = value_to_angle(v)
length, width = (MEDIUM_LEN, MEDIUM_W) if j == medium_j else (SMALL_LEN, SMALL_W)
parts.append(_tick_line(angle, length, width))
parts += [' </g>', '</svg>']
return '\n'.join(parts)