Skip to content

Commit

Permalink
fix: render high-density slider ticks correctly (#1221)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
spectrachrome committed Aug 19, 2024
1 parent cbb498d commit fc272f1
Showing 1 changed file with 193 additions and 25 deletions.
218 changes: 193 additions & 25 deletions elements/timecontrol/src/sliderticks.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import dayjs from "dayjs";
* @property {number} position
*/

/**
* @typedef {Object} YearGroup
* @property {number} year
* @property {string[]} dates
*/

/**
* @element eox-sliderticks
*/
Expand All @@ -30,6 +36,8 @@ export class SliderTicks extends LitElement {
this.svgWidth = 0;
/** @type {YearMark[]} */
this._yearMarks = [];
/** @type {YearGroup[]} */
this._years = [];
}

connectedCallback() {
Expand Down Expand Up @@ -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`
<rect
key=${yearIndex}
x=${
startPosition + barSpacing / 2
} // Add half the spacing to the start position
y="0"
width=${barWidth}
height="6"
fill="#7596A2"
></rect>
`);

// Conditionally render the year label if the bar width is sufficient
if (barWidth >= minBarWidth) {
elements.push(svg`
<text
key=${`label-${yearIndex}`}
x=${startPosition + 16}
y="26"
fill="#555"
font-size="14"
text-anchor="middle"
>
${year.year}
</text>
`);
}

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`
<line
key=${yearIndex}-${i}
x1=${position}
y1="0"
x2=${position}
y2=${date.isYearMarker ? 12 : 6}
stroke=${date.isYearMarker ? "#222" : "#7596A2"}
stroke-width="1"
></line>
`);

if (date.isYearMarker) {
elements.push(svg`
<text
key=${`label-${yearIndex}`}
x=${position + 16}
y="30"
fill="#555"
font-size="14"
text-anchor="middle"
>
${year.year}
</text>
`);
}

return elements;
});
});
}
}

/**
* @returns {number[]}
*/
Expand All @@ -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} */
Expand Down Expand Up @@ -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`
<line
key=${index}
x1=${line}
y1="0"
x2=${line}
y2=${this.isYearLine(line) ? 12 : 6}
stroke=${this.isYearLine(line) ? "#222" : "#7596A2"}
stroke-width=${this.isYearLine(line) ? 1 : 1}
></line>
`
)}
${this.yearMarks.map(
${this.sliderTicks}
${this.years.map(
(year, index) => svg`
<text
key=${`y${index}`}
x=${year.position}
y=${this.height - 1}
fill="#555"
font-size="13"
font-weight="500"
>
${year.label}
</text>
`
<text
key=${`y${index}`}
x=${year.position}
y=${this.height - 1}
fill="#555"
font-size="13"
font-weight="500"
>
${year.label}
</text>
`
)}
</svg>
</div>
Expand Down

0 comments on commit fc272f1

Please sign in to comment.