Add infinity max option for reciprocal scale
This commit is contained in:
+1
-1
@@ -15,7 +15,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: m1730-generator
|
||||
image: git.baumann.gr/adebaumann/m1730-generator:0.82
|
||||
image: git.baumann.gr/adebaumann/m1730-generator:0.83
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
|
||||
+30
-8
@@ -8,7 +8,7 @@ from fontTools.pens.svgPathPen import SVGPathPen
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
VERSION = "0.82"
|
||||
VERSION = "0.83"
|
||||
|
||||
_LOGO_PATH = pathlib.Path(__file__).parent / 'static' / 'logo.png'
|
||||
_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:
|
||||
if math.isinf(v):
|
||||
return '∞'
|
||||
# Snap near-integers — log-scale geometric values accumulate FP error
|
||||
# (e.g. 9999.9999), which would otherwise miss the integer case.
|
||||
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
|
||||
|
||||
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.")
|
||||
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.")
|
||||
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):
|
||||
return ZERO_X + SCALE_LEN * (1.0 / v - 1.0 / max_val) / k
|
||||
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 # always positive
|
||||
|
||||
def major_value(i):
|
||||
return 1.0 / (1.0 / max_val + i / big_ticks * k)
|
||||
def value_to_x(v):
|
||||
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:
|
||||
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):
|
||||
if scale_type == 'sqrt':
|
||||
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:
|
||||
v = big_vals[i] + j * (big_vals[i + 1] - big_vals[i]) / sub
|
||||
x = value_to_x(v)
|
||||
@@ -511,6 +531,8 @@ def generate():
|
||||
label_count = max(2, int(request.form.get('label_count', 6)))
|
||||
use_paths = request.form.get('output_mode', 'paths') == 'paths'
|
||||
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
|
||||
label_size = max(0.5, min(25.0, float(request.form.get('label_size', 9))))
|
||||
|
||||
@@ -330,9 +330,11 @@
|
||||
<label>Min value</label>
|
||||
<input name="min" type="number" step="any" value="0" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field" id="field-max">
|
||||
<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>
|
||||
<input type="hidden" name="scale_type" id="scale-type" value="linear">
|
||||
@@ -397,6 +399,31 @@
|
||||
<script>
|
||||
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() {
|
||||
const status = document.getElementById('status');
|
||||
status.textContent = 'Generating…';
|
||||
@@ -448,6 +475,14 @@
|
||||
if (!(parseFloat(minInput.value) > 0)) minInput.value = '1';
|
||||
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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user