Implement arc tick generation with 3 tiers across all scale mappings
This commit is contained in:
+107
@@ -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):
|
label_size=5.1126, font_key=None):
|
||||||
fd = _FONTS.get(font_key) or _FONTS.get(_DEFAULT_FONT) or next(iter(_FONTS.values()))
|
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 = (
|
font_face = (
|
||||||
'<defs><style>'
|
'<defs><style>'
|
||||||
f'@font-face {{font-family:"{fd.label}";'
|
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}" />'
|
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>']
|
parts += [' </g>', '</svg>']
|
||||||
return '\n'.join(parts)
|
return '\n'.join(parts)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user