aboutsummaryrefslogtreecommitdiff
path: root/src/components/TimeLine.vue
diff options
context:
space:
mode:
authorTerry Truong <terry06890@gmail.com>2023-01-15 22:31:04 +1100
committerTerry Truong <terry06890@gmail.com>2023-01-15 22:31:04 +1100
commitf3c97f41ee84dfdc718d1a9bc1aac24e6b6755c9 (patch)
tree7796ab0128f613c41e3ae1abf1f0545a79a1ba91 /src/components/TimeLine.vue
parent019d0f0b3d9732023272fe7deb3e22ac911d76af (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/components/TimeLine.vue')
-rw-r--r--src/components/TimeLine.vue77
1 files changed, 60 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 {