diff options
| author | Terry Truong <terry06890@gmail.com> | 2023-01-15 22:31:04 +1100 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2023-01-15 22:31:04 +1100 |
| commit | f3c97f41ee84dfdc718d1a9bc1aac24e6b6755c9 (patch) | |
| tree | 7796ab0128f613c41e3ae1abf1f0545a79a1ba91 /src | |
| parent | 019d0f0b3d9732023272fe7deb3e22ac911d76af (diff) | |
Avoid tick label overlap
Use rotation for horizontal timelines with long tick labels.
For other labels, look for overlap, and hide problematic ones.
Use darker text to indicate minor ticks instead of minor offset.
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/TimeLine.vue | 77 | ||||
| -rw-r--r-- | src/lib.ts | 9 | ||||
| -rw-r--r-- | src/store.ts | 2 |
3 files changed, 71 insertions, 17 deletions
diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index 2f3933b..7791f66 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -31,12 +31,12 @@ :style="tickStyles(tick)" class="animate-fadein" :class="{'max-tick': tick.bound == 'max', 'min-tick': tick.bound == 'min'}"/> <!-- Tick labels --> - <template v-for="tick in ticks" :key="tick.date.toInt()"> + <template v-for="tick, idx in ticks" :key="tick.date.toInt()"> <text v-if="tick.major || store.showMinorTicks" x="0" y="0" :text-anchor="vert ? 'start' : 'middle'" dominant-baseline="middle" - :fill="store.color.textDark" :style="tickLabelStyles(tick)" - class="text-sm animate-fadein cursor-default select-none"> - {{tick.date.toTickString()}} + :fill="tick.major ? store.color.textDark : store.color.textDark2" + class="text-sm animate-fadein cursor-default select-none" :style="tickLabelStyles[idx]"> + {{tickLabelTexts[idx]}} </text> </template> </svg> @@ -77,7 +77,8 @@ import CloseIcon from './icon/CloseIcon.vue'; import {WRITING_MODE_HORZ, MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES, MONTH_NAMES, MIN_CAL_DATE, getDaysInMonth, HistDate, stepDate, getScaleRatio, getNumSubUnits, getUnitDiff, getEventPrecision, dateToUnit, dateToScaleDate, - moduloPositive, TimelineState, HistEvent, getImagePath, animateWithClass} from '../lib'; + moduloPositive, TimelineState, HistEvent, getImagePath, + animateWithClass, getTextWidth} from '../lib'; import {useStore} from '../store'; import {RBTree} from '../rbtree'; @@ -1313,21 +1314,63 @@ function tickStyles(tick: Tick){ opacity: (pxOffset >= 0 && pxOffset <= availLen.value) ? 1 : 0, } } -function tickLabelStyles(tick: Tick){ +const REF_LABEL = '9999 BC'; // Used as a reference for preventing tick label overlap +const refTickLabelWidth = getTextWidth(REF_LABEL, '14px Ubuntu') + 10; +const tickLabelTexts = computed(() => ticks.value.map((tick: Tick) => tick.date.toTickString())); +const tickLabelStyles = computed((): Record<string,string>[] => { let numMajorUnits = getNumDisplayUnits(); - let pxOffset = tick.offset / numMajorUnits * availLen.value; - let pxOffset2 = tick.major ? 20 : 0; let labelSz = props.vert ? store.tickLabelHeight : tickLabelWidth.value; - return { - transform: props.vert ? - `translate(${mainlineOffset.value + tickLabelMargin.value + pxOffset2}px, ${pxOffset}px)` : - `translate(${pxOffset}px, ${mainlineOffset.value + tickLabelMargin.value + pxOffset2}px)`, - transitionProperty: skipTransition.value ? 'none' : 'transform, opacity', - transitionDuration: store.transitionDuration + 'ms', - transitionTimingFunction: 'linear', - display: (pxOffset >= labelSz && pxOffset <= availLen.value - labelSz) ? 'block' : 'none', + // Get offsets, and check for label overlap + let pxOffsets: number[] = []; + let hasLongLabel = false; // True if a label has text longer than REF_LABEL (labels will be rotated) + for (let i = 0; i < ticks.value.length; i++){ + if (tickLabelTexts.value[i].length > REF_LABEL.length){ + hasLongLabel = true; + } + pxOffsets.push(ticks.value[i].offset / numMajorUnits * availLen.value); + } + let visibilities: boolean[] = pxOffsets.map(() => true); // Elements set to false for overlapping ticks + if (!hasLongLabel && !props.vert){ + // Iterate through ticks, checking for subsequent overlapping ticks, prioritising major ticks over minor ones + for (let i = 0; i < ticks.value.length; i++){ + if (pxOffsets[i] < labelSz || pxOffsets[i] > availLen.value - labelSz){ // Hidden ticks + visibilities[i] = false; + continue; + } + if (visibilities[i] == false){ // Hidden by previous iteration + continue; + } + let tick = ticks.value[i]; + for (let j = i + 1; j < ticks.value.length; j++){ // Look at following ticks + if (pxOffsets[j] - pxOffsets[i] < refTickLabelWidth){ // Found overlap + if (!tick.major && ticks.value[j].major){ + visibilities[i] = false; + break; + } + visibilities[j] = false; + } else { + break; + } + } + } } -} + // Determine styles + let styles: Record<string,string>[] = []; + for (let i = 0; i < ticks.value.length; i++){ + let pxOffset = pxOffsets[i]; + styles.push({ + transform: props.vert ? + `translate(${mainlineOffset.value + tickLabelMargin.value}px, ${pxOffset}px)` : + `translate(${pxOffset}px, ${mainlineOffset.value + tickLabelMargin.value}px)` + + (hasLongLabel ? 'rotate(30deg)' : ''), + transitionProperty: skipTransition.value ? 'none' : 'transform, opacity', + transitionDuration: store.transitionDuration + 'ms', + transitionTimingFunction: 'linear', + display: visibilities[i] ? 'block' : 'none', + }); + } + return styles; +}); function eventStyles(eventId: number){ const [x, y, w, h] = idToPos.value.get(eventId)!; return { @@ -59,11 +59,20 @@ export function getNumTrailingZeros(n: number): number { } throw new Error('Exceeded floating point precision'); } +// Removes a class from an element, triggers reflow, then adds the class export function animateWithClass(el: HTMLElement, className: string){ el.classList.remove(className); el.offsetWidth; // Triggers reflow el.classList.add(className); } +// For estimating text width (via https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript) +const _getTextWidthCanvas = document.createElement('canvas'); +export function getTextWidth(text: string, font: string): number { + const context = _getTextWidthCanvas.getContext('2d')!; + context.font = font; + const metrics = context.measureText(text); + return metrics.width; +} // For calendar conversion (mostly copied from backend/hist_data/cal.py) export function gregorianToJdn(year: number, month: number, day: number): number { diff --git a/src/store.ts b/src/store.ts index b3fc48a..695d3a9 100644 --- a/src/store.ts +++ b/src/store.ts @@ -47,6 +47,7 @@ export type StoreState = { color: { text: string, // CSS color textDark: string, + textDark2: string, bg: string, bgLight: string, bgDark: string, @@ -68,6 +69,7 @@ function getDefaultState(): StoreState { const color = { text: '#fafaf9', // stone-50 textDark: '#a8a29e', // stone-400 + textDark2: '#68625d', // darker version of stone-500 bg: '#292524', // stone-800 bgLight: '#44403c', // stone-700 bgDark: '#1c1917', // stone-900 |
