From fc272f11911faa7f574936b2b953252069f3e48d Mon Sep 17 00:00:00 2001 From: Moritz Riede <94269527+spectrachrome@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:27:20 +0200 Subject: [PATCH] fix: render high-density slider ticks correctly (#1221) * fix: implement proper year tick rendering for very long series * temp: add longer series for testing * fix: handle year marks properly when working with large datasets * chore: remove test data * chore: format and remove unused code * chore: revert to using default data * chore: remove commented-out render fn --- elements/timecontrol/src/sliderticks.js | 218 +++++++++++++++++++++--- 1 file changed, 193 insertions(+), 25 deletions(-) diff --git a/elements/timecontrol/src/sliderticks.js b/elements/timecontrol/src/sliderticks.js index 3ba3cd3cb..20fbe40a8 100644 --- a/elements/timecontrol/src/sliderticks.js +++ b/elements/timecontrol/src/sliderticks.js @@ -7,6 +7,12 @@ import dayjs from "dayjs"; * @property {number} position */ +/** + * @typedef {Object} YearGroup + * @property {number} year + * @property {string[]} dates + */ + /** * @element eox-sliderticks */ @@ -30,6 +36,8 @@ export class SliderTicks extends LitElement { this.svgWidth = 0; /** @type {YearMark[]} */ this._yearMarks = []; + /** @type {YearGroup[]} */ + this._years = []; } connectedCallback() { @@ -59,6 +67,168 @@ export class SliderTicks extends LitElement { this.height = this.shadowRoot.querySelector("svg").clientHeight; } + /** + * Groups the dates by year. + * @returns {YearGroup[]} + */ + groupDatesByYear() { + const yearGroups = []; + + this.steps.forEach((step) => { + const date = dayjs(step); + const year = date.year(); + let yearGroup = yearGroups.find((yg) => yg.year === year); + + if (!yearGroup) { + yearGroup = { year, dates: [] }; + yearGroups.push(yearGroup); + } + + yearGroup.dates.push(step); + }); + + return yearGroups; + } + + /** + * Preprocess time strings for easier rendering. + * @returns {YearGroup[]} + */ + preprocessDates() { + const yearGroups = []; + + this.steps.forEach((step) => { + const date = dayjs(step); + const year = date.year(); + let yearGroup = yearGroups.find((yg) => yg.year === year); + + if (!yearGroup) { + yearGroup = { + year, + // How much of the total time this year represents + ratio: 0.0, + dates: [], + }; + yearGroups.push(yearGroup); + } + + yearGroup.dates.push({ + date: step, + isYearMarker: yearGroup.dates.length === 0, + }); + }); + + for (let g of yearGroups) { + g.ratio = g.dates.length / this.steps.length; + } + + return yearGroups; + } + + get sliderTicks() { + // Calculate the density (number of steps per pixel) + const density = this.steps.length / this.width; + const isHighDensity = density > 0.5; + + if (isHighDensity) { + const minBarWidth = 30; + + // High density: Render bars for each year instead of individual day ticks + const barSpacing = 2; // Adjust this value to control the spacing between bars + return this._years.flatMap((year, yearIndex) => { + // Calculate the start and end position of the bar for the year + const startPosition = + (this.steps.indexOf(year.dates[0].date) / (this.steps.length - 1)) * + this.width; + const endPosition = + (this.steps.indexOf(year.dates[year.dates.length - 1].date) / + (this.steps.length - 1)) * + this.width; + const barWidth = Math.max(0, endPosition - startPosition - barSpacing); // Subtract barSpacing from width + + const elements = []; + + // Render the year bar + elements.push(svg` + + `); + + // Conditionally render the year label if the bar width is sufficient + if (barWidth >= minBarWidth) { + elements.push(svg` + + ${year.year} + + `); + } + + return elements; + }); + } else { + return this._years.flatMap((year, yearIndex) => { + // Calculate the number of ticks that should be evenly spaced across the slider + const totalSteps = this.steps.length; + const tickInterval = Math.max(1, Math.floor(totalSteps / this.width)); // Ensure at least one tick per pixel + + return year.dates + .filter((_, dateIndex) => dateIndex % tickInterval === 0) // Filter dates to achieve even spacing + .map((date, i) => { + // Calculate position within the entire slider based on global index + const globalIndex = this.steps.indexOf(date.date); + const position = + (globalIndex / (this.steps.length - 1)) * this.width; + + const elements = []; + + elements.push(svg` + + `); + + if (date.isYearMarker) { + elements.push(svg` + + ${year.year} + + `); + } + + return elements; + }); + }); + } + } + /** * @returns {number[]} */ @@ -85,10 +255,20 @@ export class SliderTicks extends LitElement { this.requestUpdate(); } + get years() { + return this._years; + } + + set years(value) { + this._years = value; + this.requestUpdate(); + } + /** * @returns {YearMark[]} */ calculateYearMarks() { + this._years = this.preprocessDates(); /** @type {YearMark[]} */ const yearMarks = []; /** @type {number | null} */ @@ -138,32 +318,20 @@ export class SliderTicks extends LitElement { style="width: ${this.width}px; height: 30px;" viewBox="-1 0 ${this.width + 2} ${this.height}" > - ${this.lines.map( - (line, index) => svg` - - ` - )} - ${this.yearMarks.map( + ${this.sliderTicks} + ${this.years.map( (year, index) => svg` - - ${year.label} - - ` + + ${year.label} + + ` )}