Skip to content

Commit 0e30d67

Browse files
committed
feat(slider): enhance value-marker indicators with customizable properties and improved rendering logic
1 parent e0f1ea9 commit 0e30d67

File tree

4 files changed

+221
-95
lines changed

4 files changed

+221
-95
lines changed

src/cards/lcards-slider.js

Lines changed: 111 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,9 +1940,7 @@ export class LCARdSSlider extends LCARdSButton {
19401940
x="0" y="${yStart}"
19411941
width="${trackWidth}" height="${rangeHeight}"
19421942
fill="${rangeColor}"
1943-
opacity="${rangeOpacity}"
1944-
data-range-index="${idx}"
1945-
data-range-label="${rangeConfig.label || ''}" />
1943+
opacity="${rangeOpacity}" />
19461944
`;
19471945
} else {
19481946
// Horizontal: ranges extend from left to right
@@ -1955,9 +1953,7 @@ export class LCARdSSlider extends LCARdSButton {
19551953
x="${xStart}" y="0"
19561954
width="${rangeWidth}" height="${trackHeight}"
19571955
fill="${rangeColor}"
1958-
opacity="${rangeOpacity}"
1959-
data-range-index="${idx}"
1960-
data-range-label="${rangeConfig.label || ''}" />
1956+
opacity="${rangeOpacity}" />
19611957
`;
19621958
}
19631959
});
@@ -2824,6 +2820,16 @@ export class LCARdSSlider extends LCARdSButton {
28242820
trackZoneElement.innerHTML = trackContent;
28252821
}
28262822

2823+
// Append gauge value-marker indicators into the track zone (on top of ticks).
2824+
// Markers are rendered here — not in the progress zone — so they always land
2825+
// on the tick/gauge area regardless of component layout (default vs picard etc.)
2826+
if (effectiveMode === 'gauge' && trackZoneElement && trackZone) {
2827+
const markerSvg = this._generateGaugeMarkers(trackZone, orientation);
2828+
if (markerSvg) {
2829+
trackZoneElement.innerHTML += markerSvg;
2830+
}
2831+
}
2832+
28272833
// Inject progress bar into the appropriate zone.
28282834
//
28292835
// z-order is controlled by two named zone elements in the component SVG:
@@ -3008,11 +3014,11 @@ export class LCARdSSlider extends LCARdSButton {
30083014
if (isVertical) {
30093015
const yStart = zoneHeight * (1 - endPercent);
30103016
const yEnd = zoneHeight * (1 - startPercent);
3011-
svg += `<rect class="range-bg" x="0" y="${yStart}" width="${zoneWidth}" height="${yEnd - yStart}" fill="${rangeColor}" opacity="${rangeOpacity}" data-range-index="${idx}" data-range-label="${rangeConfig.label || ''}" />`;
3017+
svg += `<rect class="range-bg" x="0" y="${yStart}" width="${zoneWidth}" height="${yEnd - yStart}" fill="${rangeColor}" opacity="${rangeOpacity}" />`;
30123018
} else {
30133019
const xStart = zoneWidth * startPercent;
30143020
const xEnd = zoneWidth * endPercent;
3015-
svg += `<rect class="range-bg" x="${xStart}" y="0" width="${xEnd - xStart}" height="${zoneHeight}" fill="${rangeColor}" opacity="${rangeOpacity}" data-range-index="${idx}" data-range-label="${rangeConfig.label || ''}" />`;
3021+
svg += `<rect class="range-bg" x="${xStart}" y="0" width="${xEnd - xStart}" height="${zoneHeight}" fill="${rangeColor}" opacity="${rangeOpacity}" />`;
30163022
}
30173023
});
30183024
return svg;
@@ -3322,61 +3328,108 @@ export class LCARdSSlider extends LCARdSButton {
33223328
);
33233329
}
33243330
}
3331+
}
33253332

3326-
// Render marker indicators for value-based range entries
3327-
const markerRanges = (this._sliderStyle?.ranges || [])
3328-
.map((r, i) => ({ range: r, resolvedValue: this._resolvedMarkerValues[i] }))
3329-
.filter(({ range, resolvedValue }) => 'value' in range && resolvedValue !== null);
3330-
3331-
const dMin = this._displayConfig.min;
3332-
const dMax = this._displayConfig.max;
3333-
3334-
for (const { range, resolvedValue } of markerRanges) {
3335-
// Build indicator config: per-range settings override global indicator defaults
3336-
const globalIndicator = this._getIndicatorConfig(gaugeConfig);
3337-
const riCfg = range.indicator || {};
3338-
const markerColor = this._resolveColorValue(riCfg.color || range.color || 'var(--lcars-white, #ffffff)');
3339-
const markerIndicator = {
3340-
type: riCfg.type || globalIndicator?.type || 'line',
3341-
color: markerColor,
3342-
width: riCfg.size?.width ?? globalIndicator?.width ?? 4,
3343-
height: riCfg.size?.height ?? globalIndicator?.height ?? 25,
3344-
rotation: riCfg.rotation ?? globalIndicator?.rotation ?? 0,
3345-
offsetX: riCfg.offset?.x ?? 0,
3346-
offsetY: riCfg.offset?.y ?? 0,
3347-
borderEnabled: riCfg.border?.enabled ?? globalIndicator?.borderEnabled ?? true,
3348-
borderColor: this._resolveColorValue(riCfg.border?.color ?? 'var(--lcars-black, #000000)'),
3349-
borderWidth: riCfg.border?.width ?? globalIndicator?.borderWidth ?? 1
3350-
};
3333+
return svg;
3334+
}
33513335

3352-
const markerPercent = Math.max(0, Math.min(1, (resolvedValue - dMin) / ((dMax - dMin) || 1)));
3336+
/**
3337+
* Generate SVG for value-marker indicators, placed in the track/tick zone.
3338+
* Markers are always rendered here (not in the progress zone) so they sit on
3339+
* top of the gauge ticks regardless of component layout (default vs picard, etc.)
3340+
* @param {Object} zoneSpec - The track zone dimensions {x, y, width, height}
3341+
* @param {string} orientation - 'vertical' | 'horizontal'
3342+
* @returns {string} SVG markup
3343+
* @private
3344+
*/
3345+
_generateGaugeMarkers(zoneSpec, orientation) {
3346+
const isVertical = orientation === 'vertical';
3347+
const zoneWidth = zoneSpec.width;
3348+
const zoneHeight = zoneSpec.height;
3349+
const gaugeConfig = this._sliderStyle?.gauge;
33533350

3354-
if (isVertical) {
3355-
let baseY = height * (1 - markerPercent);
3356-
if (this._invertFill) baseY = height * markerPercent;
3357-
const mX = x + (width / 2) + markerIndicator.offsetX;
3358-
const mY = y + baseY + markerIndicator.offsetY;
3359-
svg += this._renderIndicator(
3360-
markerIndicator.type, mX, mY,
3361-
markerIndicator.width, markerIndicator.height, markerIndicator.rotation,
3362-
markerIndicator.color, markerIndicator.borderEnabled,
3363-
markerIndicator.borderColor, markerIndicator.borderWidth, true
3364-
);
3365-
} else {
3366-
let baseX = width * markerPercent;
3367-
if (this._invertFill) baseX = width * (1 - markerPercent);
3368-
const mX = x + baseX + markerIndicator.offsetX;
3369-
const mY = y + (height / 2) + markerIndicator.offsetY;
3370-
svg += this._renderIndicator(
3371-
markerIndicator.type, mX, mY,
3372-
markerIndicator.width, markerIndicator.height, markerIndicator.rotation,
3373-
markerIndicator.color, markerIndicator.borderEnabled,
3374-
markerIndicator.borderColor, markerIndicator.borderWidth, false
3375-
);
3376-
}
3351+
const markerRanges = (this._sliderStyle?.ranges || [])
3352+
.map((r, i) => ({ range: r, resolvedValue: this._resolvedMarkerValues[i] }))
3353+
.filter(({ range, resolvedValue }) => 'value' in range && resolvedValue !== null);
3354+
3355+
if (markerRanges.length === 0) return '';
3356+
3357+
const dMin = this._displayConfig.min;
3358+
const dMax = this._displayConfig.max;
3359+
let svg = '';
3360+
3361+
// Hardcoded defaults (bottom of fallback chain — preset and per-range config override these):
3362+
// Horizontal: offset.y = 10 (minor tick height). Bordered presets set
3363+
// marker_indicator.offset.y = 20 (border + tick height).
3364+
// Vertical: offset.x = 0. Border size varies; each preset that needs it
3365+
// (e.g. picard) sets the correct value in its marker_indicator block.
3366+
3367+
for (const { range, resolvedValue } of markerRanges) {
3368+
const globalIndicator = this._getIndicatorConfig(gaugeConfig);
3369+
const riCfg = range.indicator || {};
3370+
// Fallback chain: per-range indicator → style.gauge.marker_indicator (preset) → component-aware defaults
3371+
const presetMarker = gaugeConfig?.marker_indicator || {};
3372+
// Color lives under indicator.color for value markers, not range.color
3373+
// (range.color is the band fill — a separate concept)
3374+
const markerColor = this._resolveColorValue(riCfg.color || presetMarker.color || 'var(--lcars-white, #ffffff)');
3375+
const markerIndicator = {
3376+
// type: per-range config first, then triangle default.
3377+
// Deliberately does NOT inherit globalIndicator.type — the main
3378+
// indicator shape is a separate concept from a value marker.
3379+
type: riCfg.type || presetMarker.type || 'triangle',
3380+
color: markerColor,
3381+
width: riCfg.size?.width ?? presetMarker.size?.width ?? 20,
3382+
height: riCfg.size?.height ?? presetMarker.size?.height ?? 20,
3383+
rotation: riCfg.rotation ?? presetMarker.rotation ?? 180,
3384+
// align: cross-axis position within the track zone.
3385+
// 'start' → left edge (vertical) / top edge (horizontal)
3386+
// 'center' → zone centre
3387+
// 'end' → right edge (vertical) / bottom edge (horizontal)
3388+
// Default is 'start' for all components; offset adjusts tip position.
3389+
align: riCfg.align ?? presetMarker.align ?? 'start',
3390+
// Vertical: ticks are on x-axis, default offset unknown (preset must set it)
3391+
// Horizontal: ticks are on y-axis, default offset = 10 (minor tick height)
3392+
offsetX: riCfg.offset?.x ?? presetMarker.offset?.x ?? 0,
3393+
offsetY: riCfg.offset?.y ?? presetMarker.offset?.y ?? (isVertical ? 0 : 10),
3394+
borderEnabled: riCfg.border?.enabled ?? presetMarker.border?.enabled ?? false,
3395+
borderColor: this._resolveColorValue(riCfg.border?.color ?? presetMarker.border?.color ?? 'var(--lcars-black, #000000)'),
3396+
borderWidth: riCfg.border?.width ?? presetMarker.border?.width ?? 1
3397+
};
3398+
3399+
const markerPercent = Math.max(0, Math.min(1, (resolvedValue - dMin) / ((dMax - dMin) || 1)));
3400+
3401+
if (isVertical) {
3402+
// Y: value position along zone height
3403+
let mY = zoneHeight * (1 - markerPercent);
3404+
if (this._invertFill) mY = zoneHeight * markerPercent;
3405+
// X: cross-axis position determined by align, then fine-tuned by offsetX
3406+
const alignX = markerIndicator.align === 'start' ? 0
3407+
: markerIndicator.align === 'end' ? zoneWidth
3408+
: zoneWidth / 2; // 'center'
3409+
const mX = alignX + markerIndicator.offsetX;
3410+
svg += this._renderIndicator(
3411+
markerIndicator.type, mX, mY + markerIndicator.offsetY,
3412+
markerIndicator.width, markerIndicator.height, markerIndicator.rotation,
3413+
markerIndicator.color, markerIndicator.borderEnabled,
3414+
markerIndicator.borderColor, markerIndicator.borderWidth, true
3415+
);
3416+
} else {
3417+
// X: value position along zone width
3418+
let mX = zoneWidth * markerPercent;
3419+
if (this._invertFill) mX = zoneWidth * (1 - markerPercent);
3420+
// Y: cross-axis position determined by align, then fine-tuned by offsetY
3421+
const alignY = markerIndicator.align === 'start' ? 0
3422+
: markerIndicator.align === 'end' ? zoneHeight
3423+
: zoneHeight / 2; // 'center'
3424+
const mY = alignY + markerIndicator.offsetY;
3425+
svg += this._renderIndicator(
3426+
markerIndicator.type, mX + markerIndicator.offsetX, mY,
3427+
markerIndicator.width, markerIndicator.height, markerIndicator.rotation,
3428+
markerIndicator.color, markerIndicator.borderEnabled,
3429+
markerIndicator.borderColor, markerIndicator.borderWidth, false
3430+
);
33773431
}
33783432
}
3379-
33803433
return svg;
33813434
}
33823435

0 commit comments

Comments
 (0)