davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
40.2 kB
# """
# slider Layout
# """
# # Needed for the fall-back radio layout
# from potato.ai.ai_help_wrapper import get_ai_wrapper, get_dynamic_ai_help
# from .radio import generate_radio_layout
# from .identifier_utils import (
# safe_generate_layout,
# generate_element_identifier,
# escape_html_content
# )
# def test_and_get(key, d):
# val = d[key]
# try:
# return int(val)
# except:
# raise Exception(
# 'Slider scale %s\'s value for "%s" is not an int' % (d["name"], key)
# )
# def generate_slider_layout(annotation_scheme):
# """
# Generate HTML for a slider input interface.
# Args:
# annotation_scheme (dict): Configuration including:
# - name: Schema identifier
# - description: Display description
# - starting_value: Initial slider value
# - min_value: Minimum allowed value
# - max_value: Maximum allowed value
# - show_labels: Whether to show min/max labels
# - labels: If present, fall back to radio layout
# Returns:
# tuple: (html_string, key_bindings)
# html_string: Complete HTML for the slider interface
# key_bindings: Empty list (no keyboard shortcuts)
# """
# return safe_generate_layout(annotation_scheme, generate_slider_layout_internal)
# def generate_slider_layout_internal(annotation_scheme):
# from .identifier_utils import escape_html_content, generate_element_identifier
# if "labels" in annotation_scheme:
# return generate_radio_layout(annotation_scheme, horizontal=False)
# for required in ["starting_value", "min_value", "max_value"]:
# if required not in annotation_scheme:
# raise Exception(
# f'Slider scale for "{annotation_scheme["name"]}" did not include {required}'
# )
# min_value = test_and_get("min_value", annotation_scheme)
# max_value = test_and_get("max_value", annotation_scheme)
# starting_value = test_and_get("starting_value", annotation_scheme)
# if min_value >= max_value:
# raise Exception(
# f'Slider scale for "{annotation_scheme["name"]}" must have minimum value < max value ({min_value} >= {max_value})'
# )
# show_labels = annotation_scheme.get("show_labels", True)
# min_label = str(min_value) if show_labels else ''
# max_label = str(max_value) if show_labels else ''
# identifiers = generate_element_identifier(annotation_scheme["name"], "slider", "range")
# # Get step from annotation_scheme or default to 5
# step_value = annotation_scheme.get("step", 1)
# print(step_value)
# max_tick = annotation_scheme.get("maxTick", 8)
# print("max_tick")
# print(max_tick)
# schematic = f"""
# <form id="{identifiers['schema']}" class="annotation-form slider" action="javascript:void(0)" data-annotation-id="{annotation_scheme.get("annotation_id", "")}" >
# {get_ai_wrapper()}
# <fieldset schema="{identifiers['schema']}">
# <legend class="custom-slider-title">{annotation_scheme['description']}</legend>
# <div class="custom-slider-container" id="customSlider_{identifiers['id']}" tabindex="0">
# <!-- Hidden actual input for form submission -->
# <input type="range"
# min="{min_value}"
# max="{max_value}"
# step="{step_value}"
# value="{starting_value}"
# class="custom-slider-input annotation-input"
# onclick="registerAnnotation(this);"
# oninput="updateCustomSlider(this);"
# label_name="{identifiers['label_name']}"
# name="{identifiers['name']}"
# id="{identifiers['id']}"
# schema="{identifiers['schema']}">
# <!-- Custom visual elements -->
# <div class="custom-slider-track">
# <div class="custom-slider-track-active" id="sliderTrackActive_{identifiers['id']}"></div>
# <div class="custom-slider-thumb" id="sliderThumb_{identifiers['id']}"></div>
# </div>
# <div class="custom-slider-ticks" id="sliderTicks_{identifiers['id']}"></div>
# </div>
# </fieldset>
# </form>
# <script>
# // Initialize the slider on page load
# (function() {{
# const sliderId = "{identifiers['id']}";
# const sliderInput = document.getElementById(sliderId);
# if (!sliderInput) return;
# // Set up the slider initially
# setupCustomSlider(sliderInput);
# // Update positions on input change
# sliderInput.addEventListener('input', function() {{
# updateCustomSlider(this);
# }});
# }})();
# function setupCustomSlider(sliderInput) {{
# const sliderId = sliderInput.id;
# const min = parseInt(sliderInput.min);
# const max = parseInt(sliderInput.max);
# const step = parseInt(sliderInput.step) || 1;
# // Get references to custom elements
# const container = document.getElementById('customSlider_' + sliderId);
# const sliderThumb = document.getElementById('sliderThumb_' + sliderId);
# const sliderTrackActive = document.getElementById('sliderTrackActive_' + sliderId);
# const sliderTicks = document.getElementById('sliderTicks_' + sliderId);
# if (!container || !sliderThumb || !sliderTrackActive || !sliderTicks) return;
# // Initialize slider position
# updateSliderPosition({starting_value}, min, max, sliderThumb, sliderTrackActive);
# // Create tick marks
# createTicks(min, max, step, sliderTicks);
# // Add direct click handling for better UX
# container.addEventListener('click', function(e) {{
# // Skip if the click is on the input or thumb (to prevent jumps during dragging)
# if (e.target === sliderThumb || e.target === sliderInput) return;
# // Get click position relative to the track
# const rect = container.getBoundingClientRect();
# const clickPosition = e.clientX - rect.left;
# const percentClicked = (clickPosition / rect.width) * 100;
# // Calculate the new value based on the click position
# let newValue = min + (percentClicked / 100) * (max - min);
# // Snap to the nearest step
# newValue = Math.round(newValue / step) * step;
# // Ensure the value is within bounds
# newValue = Math.max(min, Math.min(max, newValue));
# // Update the input value
# sliderInput.value = newValue;
# // Update the visual position
# updateSliderPosition(newValue, min, max, sliderThumb, sliderTrackActive);
# // Trigger change event for any listeners
# const event = new Event('input', {{ bubbles: true }});
# sliderInput.dispatchEvent(event);
# // If registerAnnotation exists, call it
# if (typeof registerAnnotation === 'function') {{
# registerAnnotation(sliderInput);
# }}
# }});
# // Add keyboard control
# container.addEventListener('keydown', function(e) {{
# // Only process arrow keys
# if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
# // Prevent default to avoid page scrolling
# e.preventDefault();
# // Get current value
# let currentValue = parseFloat(sliderInput.value);
# // Update value based on arrow key
# if (e.key === 'ArrowDown') {{
# currentValue = Math.max(min, currentValue - step);
# }} else if (e.key === 'ArrowUp') {{
# currentValue = Math.min(max, currentValue + step);
# }}
# // Update the input value
# sliderInput.value = currentValue;
# // Update the visual position
# updateSliderPosition(currentValue, min, max, sliderThumb, sliderTrackActive);
# // Trigger change event for any listeners
# const event = new Event('input', {{ bubbles: true }});
# sliderInput.dispatchEvent(event);
# // If registerAnnotation exists, call it
# if (typeof registerAnnotation === 'function') {{
# registerAnnotation(sliderInput);
# }}
# }});
# // Add focus styles for keyboard users
# container.addEventListener('focus', function() {{
# container.classList.add('focus');
# }});
# container.addEventListener('blur', function() {{
# container.classList.remove('focus');
# }});
# // Add drag and hold functionality
# let isDragging = false;
# // Mouse events for drag and hold
# sliderThumb.addEventListener('mousedown', function(e) {{
# isDragging = true;
# e.preventDefault(); // Prevent text selection during drag
# // Add event listeners for drag and release
# document.addEventListener('mousemove', handleMouseDrag);
# document.addEventListener('mouseup', stopDrag);
# // Add a class to indicate dragging state
# sliderThumb.classList.add('dragging');
# }});
# // Touch events for mobile devices
# sliderThumb.addEventListener('touchstart', function(e) {{
# isDragging = true;
# e.preventDefault(); // Prevent scrolling during drag
# // Add event listeners for drag and release
# document.addEventListener('touchmove', handleTouchDrag);
# document.addEventListener('touchend', stopDrag);
# document.addEventListener('touchcancel', stopDrag);
# // Add a class to indicate dragging state
# sliderThumb.classList.add('dragging');
# }});
# // Handle mouse drag
# function handleMouseDrag(e) {{
# if (!isDragging) return;
# // Get the slider's position and dimensions
# const rect = container.getBoundingClientRect();
# // Calculate position within the slider (constrain to slider width)
# let position = e.clientX - rect.left;
# position = Math.max(0, Math.min(position, rect.width));
# // Calculate value and update slider
# updateSliderFromPosition(position, rect.width);
# }}
# // Handle touch drag
# function handleTouchDrag(e) {{
# if (!isDragging || !e.touches[0]) return;
# const touch = e.touches[0];
# const rect = container.getBoundingClientRect();
# // Calculate position within the slider (constrain to slider width)
# let position = touch.clientX - rect.left;
# position = Math.max(0, Math.min(position, rect.width));
# // Calculate value and update slider
# updateSliderFromPosition(position, rect.width);
# }}
# // Helper function to update slider from mouse/touch position
# function updateSliderFromPosition(position, width) {{
# // Convert position to a percentage
# const percent = (position / width) * 100;
# // Calculate the value based on percentage
# let newValue = min + (percent / 100) * (max - min);
# // Snap to the nearest step
# newValue = Math.round(newValue / step) * step;
# // Ensure the value is within bounds
# newValue = Math.max(min, Math.min(max, newValue));
# // Update the input value
# sliderInput.value = newValue;
# // Update the visual position
# updateSliderPosition(newValue, min, max, sliderThumb, sliderTrackActive);
# // Trigger change event
# const event = new Event('input', {{ bubbles: true }});
# sliderInput.dispatchEvent(event);
# // If registerAnnotation exists, call it
# if (typeof registerAnnotation === 'function') {{
# registerAnnotation(sliderInput);
# }}
# }}
# // Stop dragging
# function stopDrag() {{
# if (!isDragging) return;
# isDragging = false;
# sliderThumb.classList.remove('dragging');
# // Remove the event listeners
# document.removeEventListener('mousemove', handleMouseDrag);
# document.removeEventListener('touchmove', handleTouchDrag);
# document.removeEventListener('mouseup', stopDrag);
# document.removeEventListener('touchend', stopDrag);
# document.removeEventListener('touchcancel', stopDrag);
# }}
# }}
# function updateCustomSlider(sliderInput) {{
# const sliderId = sliderInput.id;
# const min = parseInt(sliderInput.min);
# const max = parseInt(sliderInput.max);
# const sliderThumb = document.getElementById('sliderThumb_' + sliderId);
# const sliderTrackActive = document.getElementById('sliderTrackActive_' + sliderId);
# if (!sliderThumb || !sliderTrackActive) return;
# updateSliderPosition(sliderInput.value, min, max, sliderThumb, sliderTrackActive);
# }}
# function updateSliderPosition(value, min, max, thumbElement, trackElement) {{
# const percent = ((value - min) / (max - min)) * 100;
# thumbElement.style.left = `${{percent}}%`;
# trackElement.style.width = `${{percent}}%`;
# }}
# function createTicks(min, max, step, tickContainer) {{
# // Clear existing ticks
# tickContainer.innerHTML = '';
# // Calculate optimal ticks
# const ticks = calculateOptimalTicks(min, max, step);
# ticks.forEach(tick => {{
# const percent = ((tick.value - min) / (max - min)) * 100;
# const tickElement = document.createElement('div');
# tickElement.className = 'custom-slider-tick';
# tickElement.style.left = `${{percent}}%`;
# const tickMark = document.createElement('div');
# tickMark.className = tick.showLabel ? 'custom-slider-tick-mark major' : 'custom-slider-tick-mark';
# tickElement.appendChild(tickMark);
# if (tick.showLabel) {{
# const tickLabel = document.createElement('div');
# tickLabel.className = 'custom-slider-tick-label';
# tickLabel.textContent = tick.value;
# tickElement.appendChild(tickLabel);
# }}
# tickContainer.appendChild(tickElement);
# }});
# }}
# function calculateOptimalTicks(min, max, sliderStep, maxTicks = {max_tick}) {{
# const range = max - min;
# // Generate all possible slider positions based on the step
# const possibleValues = [];
# for (let value = min; value <= max; value += sliderStep) {{
# possibleValues.push(Math.round(value));
# }}
# // If we have few enough values, show them all
# if (possibleValues.length <= maxTicks) {{
# return possibleValues.map(value => ({{ value, showLabel: true }}));
# }}
# // Calculate a "nice" interval for major ticks
# const targetInterval = range / (maxTicks - 1);
# // Find nice intervals: 1, 2, 5, 10, 20, 50, 100, etc.
# const candidates = [];
# for (let magnitude = 1; magnitude <= range; magnitude *= 10) {{
# candidates.push(magnitude); // 1, 10, 100...
# candidates.push(2 * magnitude); // 2, 20, 200...
# candidates.push(5 * magnitude); // 5, 50, 500...
# }}
# // Choose the candidate closest to our target
# let bestInterval = candidates[0];
# let bestDiff = Math.abs(candidates[0] - targetInterval);
# for (const candidate of candidates) {{
# const diff = Math.abs(candidate - targetInterval);
# if (diff < bestDiff) {{
# bestInterval = candidate;
# bestDiff = diff;
# }}
# }}
# // Generate major ticks using the best interval
# const majorTicks = [];
# // Always start with min
# majorTicks.push({{ value: min, showLabel: true }});
# // Add ticks at nice intervals
# for (let tickValue = bestInterval; tickValue < max; tickValue += bestInterval) {{
# if (tickValue > min) {{
# majorTicks.push({{ value: tickValue, showLabel: true }});
# }}
# }}
# // Always end with max (if it's not the same as the last tick)
# if (majorTicks[majorTicks.length - 1].value !== max) {{
# majorTicks.push({{ value: max, showLabel: true }});
# }}
# // Add minor ticks for values between major ticks
# const allTicks = [...majorTicks];
# // Only add minor ticks if there's reasonable spacing
# if (majorTicks.length > 1) {{
# const majorSpacing = bestInterval;
# if (majorSpacing > sliderStep * 2) {{
# possibleValues.forEach(value => {{
# if (!allTicks.some(t => t.value === value)) {{
# allTicks.push({{ value, showLabel: false }});
# }}
# }});
# }}
# }}
# return allTicks.sort((a, b) => a.value - b.value);
# }}
# </script>
# """
# key_bindings = []
# return schematic, key_bindings
"""
slider Layout
"""
import logging
# Needed for the fall-back radio layout
from potato.ai.ai_help_wrapper import get_ai_wrapper, get_dynamic_ai_help
from .radio import generate_radio_layout
from .identifier_utils import (
safe_generate_layout,
generate_element_identifier,
escape_html_content
)
logger = logging.getLogger(__name__)
def test_and_get(key, d):
val = d[key]
try:
return int(val)
except:
raise Exception(
'Slider scale %s\'s value for "%s" is not an int' % (d["name"], key)
)
def generate_slider_layout(annotation_scheme):
"""
Generate HTML for a slider input interface.
Args:
annotation_scheme (dict): Configuration including:
- name: Schema identifier
- description: Display description
- starting_value: Initial slider value
- min_value: Minimum allowed value
- max_value: Maximum allowed value
- show_labels: Whether to show min/max labels
- labels: If present, fall back to radio layout
Returns:
tuple: (html_string, key_bindings)
html_string: Complete HTML for the slider interface
key_bindings: Empty list (no keyboard shortcuts)
"""
return safe_generate_layout(annotation_scheme, generate_slider_layout_internal)
def generate_slider_layout_internal(annotation_scheme):
from .identifier_utils import escape_html_content, generate_element_identifier, generate_validation_attribute
if "labels" in annotation_scheme:
return generate_radio_layout(annotation_scheme, horizontal=False)
for required in ["starting_value", "min_value", "max_value"]:
if required not in annotation_scheme:
raise Exception(
f'Slider scale for "{annotation_scheme["name"]}" did not include {required}'
)
min_value = test_and_get("min_value", annotation_scheme)
max_value = test_and_get("max_value", annotation_scheme)
starting_value = test_and_get("starting_value", annotation_scheme)
if min_value >= max_value:
raise Exception(
f'Slider scale for "{annotation_scheme["name"]}" must have minimum value < max value ({min_value} >= {max_value})'
)
show_labels = annotation_scheme.get("show_labels", True)
min_label = str(min_value) if show_labels else ''
max_label = str(max_value) if show_labels else ''
identifiers = generate_element_identifier(annotation_scheme["name"], "slider", "range")
validation = generate_validation_attribute(annotation_scheme)
# Get step from annotation_scheme or default to 1
step_value = annotation_scheme.get("step", 1)
max_tick = annotation_scheme.get("maxTick", 8)
schematic = f"""
<form id="{identifiers['schema']}" class="annotation-form slider" action="javascript:void(0)" data-annotation-id="{escape_html_content(str(annotation_scheme.get("annotation_id", "")))}" >
{get_ai_wrapper()}
<fieldset schema="{identifiers['schema']}">
<legend class="custom-slider-title">{annotation_scheme['description']}</legend>
<div class="custom-slider-container" id="customSlider_{identifiers['id']}" tabindex="0">
<!-- Hidden actual input for form submission -->
<input type="range"
min="{min_value}"
max="{max_value}"
step="{step_value}"
value="{starting_value}"
class="custom-slider-input annotation-input"
onclick="registerAnnotation(this);"
oninput="updateCustomSlider(this);"
label_name="{identifiers['label_name']}"
name="{identifiers['name']}"
id="{identifiers['id']}"
schema="{identifiers['schema']}"
validation="{validation}">
<!-- Custom visual elements -->
<div class="custom-slider-track">
<div class="custom-slider-track-active" id="sliderTrackActive_{identifiers['id']}"></div>
<div class="custom-slider-thumb" id="sliderThumb_{identifiers['id']}">
<!-- Tooltip attached directly to thumb -->
<div class="slider-tooltip" id="sliderTooltip_{identifiers['id']}">{starting_value}</div>
</div>
</div>
<div class="custom-slider-ticks" id="sliderTicks_{identifiers['id']}"></div>
</div>
</fieldset>
</form>
<script>
// Initialize the slider on page load
(function() {{
const sliderId = "{identifiers['id']}";
const sliderInput = document.getElementById(sliderId);
if (!sliderInput) return;
// Set up the slider initially
setupCustomSlider(sliderInput);
// Update positions on input change
sliderInput.addEventListener('input', function() {{
updateCustomSlider(this);
}});
}})();
function setupCustomSlider(sliderInput) {{
const sliderId = sliderInput.id;
const min = parseInt(sliderInput.min);
const max = parseInt(sliderInput.max);
const step = parseInt(sliderInput.step) || 1;
// Get references to custom elements
const container = document.getElementById('customSlider_' + sliderId);
const sliderThumb = document.getElementById('sliderThumb_' + sliderId);
const sliderTrackActive = document.getElementById('sliderTrackActive_' + sliderId);
const sliderTicks = document.getElementById('sliderTicks_' + sliderId);
const tooltip = document.getElementById('sliderTooltip_' + sliderId);
if (!container || !sliderThumb || !sliderTrackActive || !sliderTicks || !tooltip) return;
// Initialize slider position
updateSliderPosition({starting_value}, min, max, sliderThumb, sliderTrackActive, tooltip);
// Create tick marks
createTicks(min, max, step, sliderTicks);
container.addEventListener('click', function(e) {{
// Skip if the click is on the input or thumb (to prevent jumps during dragging)
if (e.target === sliderThumb || e.target === sliderInput) return;
// Get click position relative to the track
const rect = container.getBoundingClientRect();
const clickPosition = e.clientX - rect.left;
const percentClicked = (clickPosition / rect.width) * 100;
// Calculate the new value based on the click position
let newValue = min + (percentClicked / 100) * (max - min);
// Snap to the nearest step
newValue = Math.round(newValue / step) * step;
// Ensure the value is within bounds
newValue = Math.max(min, Math.min(max, newValue));
// Update the input value
sliderInput.value = newValue;
// Update the visual position
updateSliderPosition(newValue, min, max, sliderThumb, sliderTrackActive, tooltip);
// Trigger change event for any listeners
const event = new Event('input', {{ bubbles: true }});
sliderInput.dispatchEvent(event);
// If registerAnnotation exists, call it
if (typeof registerAnnotation === 'function') {{
registerAnnotation(sliderInput);
}}
}});
// Add keyboard control
container.addEventListener('keydown', function(e) {{
// Only process arrow keys
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
// Prevent default to avoid page scrolling
e.preventDefault();
// Get current value
let currentValue = parseFloat(sliderInput.value);
// Update value based on arrow key
if (e.key === 'ArrowDown') {{
currentValue = Math.max(min, currentValue - step);
}} else if (e.key === 'ArrowUp') {{
currentValue = Math.min(max, currentValue + step);
}}
// Update the input value
sliderInput.value = currentValue;
// Update the visual position
updateSliderPosition(currentValue, min, max, sliderThumb, sliderTrackActive, tooltip);
// Trigger change event for any listeners
const event = new Event('input', {{ bubbles: true }});
sliderInput.dispatchEvent(event);
// If registerAnnotation exists, call it
if (typeof registerAnnotation === 'function') {{
registerAnnotation(sliderInput);
}}
}});
// Add focus styles for keyboard users
container.addEventListener('focus', function() {{
container.classList.add('focus');
}});
container.addEventListener('blur', function() {{
container.classList.remove('focus');
}});
// Add drag and hold functionality
let isDragging = false;
// Mouse events for drag and hold
sliderThumb.addEventListener('mousedown', function(e) {{
isDragging = true;
e.preventDefault(); // Prevent text selection during drag
// Show tooltip during drag
sliderThumb.classList.add('dragging');
// Add event listeners for drag and release
document.addEventListener('mousemove', handleMouseDrag);
document.addEventListener('mouseup', stopDrag);
}});
// Touch events for mobile devices
sliderThumb.addEventListener('touchstart', function(e) {{
isDragging = true;
e.preventDefault(); // Prevent scrolling during drag
// Show tooltip during drag
sliderThumb.classList.add('dragging');
// Add event listeners for drag and release
document.addEventListener('touchmove', handleTouchDrag);
document.addEventListener('touchend', stopDrag);
document.addEventListener('touchcancel', stopDrag);
}});
// Handle mouse drag
function handleMouseDrag(e) {{
if (!isDragging) return;
// Get the slider's position and dimensions
const rect = container.getBoundingClientRect();
// Calculate position within the slider (constrain to slider width)
let position = e.clientX - rect.left;
position = Math.max(0, Math.min(position, rect.width));
// Calculate value and update slider
updateSliderFromPosition(position, rect.width);
}}
// Handle touch drag
function handleTouchDrag(e) {{
if (!isDragging || !e.touches[0]) return;
const touch = e.touches[0];
const rect = container.getBoundingClientRect();
// Calculate position within the slider (constrain to slider width)
let position = touch.clientX - rect.left;
position = Math.max(0, Math.min(position, rect.width));
// Calculate value and update slider
updateSliderFromPosition(position, rect.width);
}}
// Helper function to update slider from mouse/touch position
function updateSliderFromPosition(position, width) {{
// Convert position to a percentage
const percent = (position / width) * 100;
// Calculate the value based on percentage
let newValue = min + (percent / 100) * (max - min);
// Snap to the nearest step
newValue = Math.round(newValue / step) * step;
// Ensure the value is within bounds
newValue = Math.max(min, Math.min(max, newValue));
// Update the input value
sliderInput.value = newValue;
// Update the visual position
updateSliderPosition(newValue, min, max, sliderThumb, sliderTrackActive, tooltip);
// Trigger change event
const event = new Event('input', {{ bubbles: true }});
sliderInput.dispatchEvent(event);
// If registerAnnotation exists, call it
if (typeof registerAnnotation === 'function') {{
registerAnnotation(sliderInput);
}}
}}
// Stop dragging
function stopDrag() {{
if (!isDragging) return;
isDragging = false;
sliderThumb.classList.remove('dragging');
// Remove the event listeners
document.removeEventListener('mousemove', handleMouseDrag);
document.removeEventListener('touchmove', handleTouchDrag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchend', stopDrag);
document.removeEventListener('touchcancel', stopDrag);
}}
}}
function updateCustomSlider(sliderInput) {{
const sliderId = sliderInput.id;
const min = parseInt(sliderInput.min);
const max = parseInt(sliderInput.max);
const sliderThumb = document.getElementById('sliderThumb_' + sliderId);
const sliderTrackActive = document.getElementById('sliderTrackActive_' + sliderId);
const tooltip = document.getElementById('sliderTooltip_' + sliderId);
if (!sliderThumb || !sliderTrackActive || !tooltip) return;
updateSliderPosition(sliderInput.value, min, max, sliderThumb, sliderTrackActive, tooltip);
}}
function updateSliderPosition(value, min, max, thumbElement, trackElement, tooltipElement) {{
const percent = ((value - min) / (max - min)) * 100;
thumbElement.style.left = `${{percent}}%`;
trackElement.style.width = `${{percent}}%`;
// Update tooltip content only - it moves with the thumb automatically
if (tooltipElement) {{
tooltipElement.textContent = value;
}}
}}
function createTicks(min, max, step, tickContainer) {{
// Clear existing ticks
tickContainer.innerHTML = '';
// Calculate optimal ticks
const ticks = calculateOptimalTicks(min, max, step);
ticks.forEach(tick => {{
const percent = ((tick.value - min) / (max - min)) * 100;
const tickElement = document.createElement('div');
tickElement.className = 'custom-slider-tick';
tickElement.style.left = `${{percent}}%`;
const tickMark = document.createElement('div');
tickMark.className = tick.showLabel ? 'custom-slider-tick-mark major' : 'custom-slider-tick-mark';
tickElement.appendChild(tickMark);
if (tick.showLabel) {{
const tickLabel = document.createElement('div');
tickLabel.className = 'custom-slider-tick-label';
tickLabel.textContent = tick.value;
tickElement.appendChild(tickLabel);
}}
tickContainer.appendChild(tickElement);
}});
}}
function calculateOptimalTicks(min, max, sliderStep, maxTicks = {max_tick}) {{
const range = max - min;
// Generate all possible slider positions based on the step
const possibleValues = [];
for (let value = min; value <= max; value += sliderStep) {{
possibleValues.push(Math.round(value));
}}
// If we have few enough values, show them all
if (possibleValues.length <= maxTicks) {{
return possibleValues.map(value => ({{ value, showLabel: true }}));
}}
// Calculate a "nice" interval for major ticks
const targetInterval = range / (maxTicks - 1);
// Find nice intervals: 1, 2, 5, 10, 20, 50, 100, etc.
const candidates = [];
for (let magnitude = 1; magnitude <= range; magnitude *= 10) {{
candidates.push(magnitude); // 1, 10, 100...
candidates.push(2 * magnitude); // 2, 20, 200...
candidates.push(5 * magnitude); // 5, 50, 500...
}}
// Choose the candidate closest to our target
let bestInterval = candidates[0];
let bestDiff = Math.abs(candidates[0] - targetInterval);
for (const candidate of candidates) {{
const diff = Math.abs(candidate - targetInterval);
if (diff < bestDiff) {{
bestInterval = candidate;
bestDiff = diff;
}}
}}
// Generate major ticks using the best interval
const majorTicks = [];
// Always start with min
majorTicks.push({{ value: min, showLabel: true }});
// Add ticks at nice intervals
for (let tickValue = bestInterval; tickValue < max; tickValue += bestInterval) {{
if (tickValue > min) {{
majorTicks.push({{ value: tickValue, showLabel: true }});
}}
}}
// Always end with max (if it's not the same as the last tick)
if (majorTicks[majorTicks.length - 1].value !== max) {{
majorTicks.push({{ value: max, showLabel: true }});
}}
// Add minor ticks for values between major ticks
const allTicks = [...majorTicks];
// Only add minor ticks if there's reasonable spacing
if (majorTicks.length > 1) {{
const majorSpacing = bestInterval;
if (majorSpacing > sliderStep * 2) {{
possibleValues.forEach(value => {{
if (!allTicks.some(t => t.value === value)) {{
allTicks.push({{ value, showLabel: false }});
}}
}});
}}
}}
return allTicks.sort((a, b) => a.value - b.value);
}}
</script>
"""
key_bindings = []
return schematic, key_bindings