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}
+
+ `
)}