Add infinity max option for reciprocal scale

This commit is contained in:
2026-07-14 23:28:40 +02:00
parent cf844d007b
commit f19c462c1b
3 changed files with 68 additions and 11 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ spec:
spec: spec:
containers: containers:
- name: m1730-generator - name: m1730-generator
image: git.baumann.gr/adebaumann/m1730-generator:0.82 image: git.baumann.gr/adebaumann/m1730-generator:0.83
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 5000 - containerPort: 5000
+30 -8
View File
@@ -8,7 +8,7 @@ from fontTools.pens.svgPathPen import SVGPathPen
app = Flask(__name__) app = Flask(__name__)
VERSION = "0.82" VERSION = "0.83"
_LOGO_PATH = pathlib.Path(__file__).parent / 'static' / 'logo.png' _LOGO_PATH = pathlib.Path(__file__).parent / 'static' / 'logo.png'
_LOGO_B64 = base64.b64encode(_LOGO_PATH.read_bytes()).decode() _LOGO_B64 = base64.b64encode(_LOGO_PATH.read_bytes()).decode()
@@ -291,6 +291,8 @@ def _unit_font_size(unit: str) -> float:
def _fmt(v: float) -> str: def _fmt(v: float) -> str:
if math.isinf(v):
return ''
# Snap near-integers — log-scale geometric values accumulate FP error # Snap near-integers — log-scale geometric values accumulate FP error
# (e.g. 9999.9999), which would otherwise miss the integer case. # (e.g. 9999.9999), which would otherwise miss the integer case.
r = round(v) r = round(v)
@@ -363,17 +365,31 @@ def generate_svg(unit, min_val, max_val, range_label, big_ticks, small_ticks, la
return min_val + span * (i / big_ticks) ** 2 return min_val + span * (i / big_ticks) ** 2
elif scale_type == 'reciprocal': elif scale_type == 'reciprocal':
if min_val <= 0 or max_val <= 0: if min_val <= 0:
raise ValueError("Reciprocal scale requires min and max values greater than 0.") raise ValueError("Reciprocal scale requires min and max values greater than 0.")
if max_val <= min_val: if not math.isinf(max_val) and max_val <= min_val:
raise ValueError("Reciprocal scale requires max value greater than min.") raise ValueError("Reciprocal scale requires max value greater than min.")
k = 1.0 / min_val - 1.0 / max_val # always positive if math.isinf(max_val):
# Simplified form when max = ∞: x = ZERO_X + SCALE_LEN * min_val / v
def value_to_x(v):
if math.isinf(v):
return ZERO_X
return ZERO_X + SCALE_LEN * min_val / v
def value_to_x(v): def major_value(i):
return ZERO_X + SCALE_LEN * (1.0 / v - 1.0 / max_val) / k 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 # always positive
def major_value(i): def value_to_x(v):
return 1.0 / (1.0 / max_val + i / big_ticks * k) return ZERO_X + SCALE_LEN * (1.0 / v - 1.0 / max_val) / k
def major_value(i):
return 1.0 / (1.0 / max_val + i / big_ticks * k)
else: else:
span = max_val - min_val span = max_val - min_val
@@ -471,6 +487,10 @@ def generate_svg(unit, min_val, max_val, range_label, big_ticks, small_ticks, la
for j in range(1, small_ticks + 1): for j in range(1, small_ticks + 1):
if scale_type == 'sqrt': if scale_type == 'sqrt':
x = big_xs[i] + (j / sub) ** 2 * (big_xs[i + 1] - big_xs[i]) x = big_xs[i] + (j / sub) ** 2 * (big_xs[i + 1] - big_xs[i])
elif math.isinf(big_vals[i]):
# First interval with ∞ at the left: interpolate in position space
# (linear value interpolation is undefined from ∞).
x = big_xs[i] + (j / sub) * (big_xs[i + 1] - big_xs[i])
else: else:
v = big_vals[i] + j * (big_vals[i + 1] - big_vals[i]) / sub v = big_vals[i] + j * (big_vals[i + 1] - big_vals[i]) / sub
x = value_to_x(v) x = value_to_x(v)
@@ -511,6 +531,8 @@ def generate():
label_count = max(2, int(request.form.get('label_count', 6))) label_count = max(2, int(request.form.get('label_count', 6)))
use_paths = request.form.get('output_mode', 'paths') == 'paths' use_paths = request.form.get('output_mode', 'paths') == 'paths'
scale_type = request.form.get('scale_type', 'linear') scale_type = request.form.get('scale_type', 'linear')
if request.form.get('max_is_inf') == '1':
max_val = math.inf
custom_labels = [s.strip() for s in request.form.get('labels', '').split(',') if s.strip()] or None custom_labels = [s.strip() for s in request.form.get('labels', '').split(',') if s.strip()] or None
label_size = max(0.5, min(25.0, float(request.form.get('label_size', 9)))) label_size = max(0.5, min(25.0, float(request.form.get('label_size', 9))))
+37 -2
View File
@@ -330,9 +330,11 @@
<label>Min value</label> <label>Min value</label>
<input name="min" type="number" step="any" value="0" required> <input name="min" type="number" step="any" value="0" required>
</div> </div>
<div class="field"> <div class="field" id="field-max">
<label>Max value</label> <label>Max value</label>
<input name="max" type="number" step="any" value="100" required> <input name="max" id="input-max" type="number" step="any" value="100" required>
<button type="button" id="btn-inf" style="display:none;font-family:var(--f-mono);font-size:0.65rem;color:var(--muted);background:none;border:none;padding:0;cursor:pointer;text-align:left">set max = ∞</button>
<input type="hidden" name="max_is_inf" id="max-is-inf" value="">
</div> </div>
</div> </div>
<input type="hidden" name="scale_type" id="scale-type" value="linear"> <input type="hidden" name="scale_type" id="scale-type" value="linear">
@@ -397,6 +399,31 @@
<script> <script>
let lastSvg = ''; let lastSvg = '';
function resetInf() {
const inp = document.getElementById('input-max');
const hidden = document.getElementById('max-is-inf');
const btn = document.getElementById('btn-inf');
inp.style.display = '';
inp.required = true;
hidden.value = '';
btn.textContent = 'set max = ∞';
}
document.getElementById('btn-inf').addEventListener('click', () => {
const inp = document.getElementById('input-max');
const hidden = document.getElementById('max-is-inf');
const btn = document.getElementById('btn-inf');
if (hidden.value === '1') {
resetInf();
} else {
inp.style.display = 'none';
inp.required = false;
hidden.value = '1';
btn.textContent = 'max = ∞ (click to reset)';
}
doGenerate();
});
async function doGenerate() { async function doGenerate() {
const status = document.getElementById('status'); const status = document.getElementById('status');
status.textContent = 'Generating…'; status.textContent = 'Generating…';
@@ -448,6 +475,14 @@
if (!(parseFloat(minInput.value) > 0)) minInput.value = '1'; if (!(parseFloat(minInput.value) > 0)) minInput.value = '1';
if (!(parseFloat(maxInput.value) > parseFloat(minInput.value))) maxInput.value = String(parseFloat(minInput.value) * 10); if (!(parseFloat(maxInput.value) > parseFloat(minInput.value))) maxInput.value = String(parseFloat(minInput.value) * 10);
} }
// Show/hide ∞ toggle
const infBtn = document.getElementById('btn-inf');
if (btn.dataset.scale === 'reciprocal') {
infBtn.style.display = '';
} else {
infBtn.style.display = 'none';
resetInf();
}
} }
doGenerate(); doGenerate();
}); });