Add infinity max option for reciprocal scale
This commit is contained in:
+1
-1
@@ -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
@@ -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))))
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user