diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/TimeLine.vue | 239 |
1 files changed, 160 insertions, 79 deletions
diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index 413c163..312aec6 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -64,7 +64,7 @@ import IconButton from './IconButton.vue'; import CloseIcon from './icon/CloseIcon.vue'; // Other import {WRITING_MODE_HORZ, MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES, - MIN_CAL_YEAR, HistDate, CalDate, stepDate, inDateScale, getScaleRatio, getUnitDiff, getDaysInMonth, + MIN_CAL_YEAR, HistDate, CalDate, stepDate, getScaleRatio, getUnitDiff, getDaysInMonth, moduloPositive, TimelineState, HistEvent, getImagePath} from '../lib'; import {useStore} from '../store'; import {RBTree} from '../rbtree'; @@ -134,14 +134,30 @@ const mainlineOffset = computed(() => { // Distance from mainline-area line to l // Timeline data const ID = props.initialState.id as number; const MIN_CAL_DATE = new CalDate(MIN_CAL_YEAR, 1, 1); -const startDate = ref(props.initialState.startDate); // Earliest date to display -const endDate = ref(props.initialState.endDate); +const startDate = ref(props.initialState.startDate); // Earliest date of scale to display +const endDate = ref(props.initialState.endDate); // Latest date of scale to display (may equal startDate) const startOffset = ref(store.defaultEndTickOffset); // Fraction of a scale unit before startDate to show // Note: Without this, the timeline can only move if the distance is over one unit, which makes dragging awkward, // can cause unexpected jumps when zooming, and limits display when a unit has many ticks on the next scale const endOffset = ref(store.defaultEndTickOffset); const scaleIdx = ref(0); // Index of current scale in SCALES const scale = computed(() => SCALES[scaleIdx.value]) +const hasMinorScale = computed(() => { // If true, display subset of ticks of next lower scale + let yearlyScaleOnly = startDate.value.isEarlier(MIN_CAL_DATE); + if (scale.value == DAY_SCALE || yearlyScaleOnly && scale.value == 1){ + return false; + } + let numUnits: number; + if (scale.value >= 1){ + let yearDiff = startDate.value.getYearDiff(endDate.value); + numUnits = yearDiff / scale.value + startOffset.value + endOffset.value; + } else { //scale.value == MONTH_SCALE + let monthDiff = startDate.value.getMonthDiff(endDate.value); + numUnits = monthDiff + startOffset.value + endOffset.value; + } + return (availLen.value / numUnits) >= 2 * store.minTickSep; +}); +const minorScale = computed(() => hasMinorScale.value ? SCALES[scaleIdx.value + 1] : scale.value); if (props.initialState.startOffset != null){ startOffset.value = props.initialState.startOffset as number; } @@ -155,42 +171,50 @@ if (props.initialState.scaleIdx != null){ } // function initScale(){ // Initialises to smallest usable scale - if (startDate.value.isEarlier(MIN_CAL_DATE)){ // If unable to use JDNs, use a yearly scale - scaleIdx.value = getYearlyScale(startDate.value, endDate.value, availLen.value); - } else { - let dayDiff = startDate.value.getDayDiff(endDate.value) + startOffset.value + endOffset.value; - // Check for day scale usability - if (availLen.value / dayDiff >= store.minTickSep){ - scaleIdx.value = SCALES.findIndex(s => s == DAY_SCALE); - } else { - // Check for month scale usability - let monthDiff = startDate.value.getMonthDiff(endDate.value) + startOffset.value + endOffset.value; - if (availLen.value / monthDiff >= store.minTickSep){ - scaleIdx.value = SCALES.findIndex(s => s == MONTH_SCALE); - } else { // Use a yearly scale - scaleIdx.value = getYearlyScale(startDate.value, endDate.value, availLen.value); + let yearlyScaleOnly = startDate.value.isEarlier(MIN_CAL_DATE); + let yearDiff = startDate.value.getYearDiff(endDate.value); + let monthDiff = startDate.value.getMonthDiff(endDate.value); + // Get largest scale with units no larger than startDate-to-endDate range + let idx = 0; + if (yearDiff > 0){ + while (SCALES[idx] > yearDiff){ + if (yearlyScaleOnly && SCALES[idx] == 1){ + break; } + idx += 1; } + } else if (monthDiff > 0){ + idx = SCALES.findIndex(s => s == MONTH_SCALE); + } else { + idx = SCALES.findIndex(s => s == DAY_SCALE); } - onStateChg(); -} -function getYearlyScale(startDate: HistDate, endDate: HistDate, availLen: number){ - // Get the smallest yearly scale that divides a date range, without making ticks too close - let yearDiff = startDate.getYearDiff(endDate); - let idx = 0; - while (SCALES[idx] >= yearDiff){ // Get scale with units smaller than yearDiff - idx += 1; - } - while (idx < SCALES.length - 1){ // Check for usable smaller scales - let nextScale = SCALES[idx + 1] - let adjustedYearDiff = yearDiff + startOffset.value * nextScale + endOffset.value * nextScale; - if (availLen / (adjustedYearDiff / nextScale) >= store.minTickSep){ + // Check for usable smaller scale + while (SCALES[idx] > 1){ + let nextScale = SCALES[idx + 1]; + let numUnits = yearDiff / nextScale + startOffset.value + endOffset.value; + if (availLen.value / numUnits >= store.minTickSep){ idx += 1; } else { break; } } - return idx; + if (!yearlyScaleOnly){ + if (SCALES[idx] == 1){ + let numUnits = monthDiff + startOffset.value + endOffset.value; + if (availLen.value / numUnits >= store.minTickSep){ + idx += 1; + } + } + if (SCALES[idx] == MONTH_SCALE){ + let numUnits = startDate.value.getDayDiff(endDate.value) + startOffset.value + endOffset.value; + if (availLen.value / numUnits >= store.minTickSep){ + idx += 1; + } + } + } + // + scaleIdx.value = idx; + onStateChg(); } // Tick data @@ -202,59 +226,108 @@ type Ticks = { // Index of first visible tick (hidden ticks are used for smoother transitions) // Ignored if 'dates' is empty endIdx: number, // Index of last visible tick + firstMajorIdx: number, // Index of first major tick + minorsPerMajor: number, // Minor ticks per major tick + minorTickStep: number, // Number of minor units between minor ticks }; -function getNumVisibleUnits(): number { +function getNumDisplayUnits({inclOffsets=true} = {}): number { // Get num major units in display range let unitDiff = getUnitDiff(startDate.value, endDate.value, scale.value); - return unitDiff + startOffset.value + endOffset.value; + if (inclOffsets){ + unitDiff += startOffset.value + endOffset.value; + } + return unitDiff; } const ticks = computed((): Ticks => { if (!mounted.value){ - return {dates: [], startIdx: 0, endIdx: 0}; + return {dates: [], startIdx: 0, endIdx: 0, firstMajorIdx: 0, minorsPerMajor: 0, minorTickStep: 0}; } - let numUnits = getNumVisibleUnits(); + // Ticks fields let dates: HistDate[] = []; let startIdx: number; let endIdx: number; - // Get hidden preceding ticks - let panUnits = Math.floor(numUnits * store.scrollRatio); // Potential distance shifted upon a pan action - if (MIN_DATE.isEarlier(startDate.value, scale.value)){ - let date = startDate.value; - for (let i = 0; i < panUnits; i++){ - date = stepDate(date, scale.value, {forward: false}); - dates.push(date); - if (MIN_DATE.equals(date, scale.value)){ - break; - } + let firstMajorIdx: number; + let minorsPerMajor: number; + let minorTickStep: number; + // Determine minor-tick fields + let numUnits = getNumDisplayUnits(); + if (hasMinorScale.value){ + let minorUnitsPerMajor = getScaleRatio(minorScale.value, scale.value, true); + let majorUnitSz = availLen.value / getNumDisplayUnits(); + let minorUnitSz = majorUnitSz / minorUnitsPerMajor; + minorTickStep = Math.ceil(store.minTickSep / minorUnitSz); + minorsPerMajor = Math.ceil(minorUnitsPerMajor / minorTickStep) - 1; + } else { + minorTickStep = 0; + minorsPerMajor = 0; + } + // Get before-startDate ticks (including start-offset ticks and hidden ticks) + let panUnits = Math.floor(getNumDisplayUnits() * store.scrollRatio); // Potential shift distance upon a pan action + let date = startDate.value; + for (let i = 0; i < panUnits + Math.ceil(startOffset.value); i++){ + if (MIN_DATE.equals(date, scale.value)){ + break; + } + date = stepDate(date, scale.value, {forward: false}); + // Add minor dates + let minorDates: HistDate[] = []; + let tempDate = date; + for (let j = 0; j < minorsPerMajor; j++){ + tempDate = stepDate(tempDate, minorScale.value, {count: minorTickStep}); + minorDates.push(tempDate); } - dates.reverse(); + minorDates.reverse(); + dates.push(...minorDates); + // Add major date + dates.push(date); } + dates.reverse(); startIdx = dates.length; - // Get visible ticks - let date = startDate.value.clone() - dates.push(date); - for (let i = 0; i < Math.round(numUnits - startOffset.value - endOffset.value); i++){ - date = stepDate(date, scale.value); + // Get startDate-to-endDate ticks + date = startDate.value.clone(); + firstMajorIdx = -1; + let numMajorUnits = getNumDisplayUnits({inclOffsets: false}); + for (let i = 0; i <= numMajorUnits; i++){ dates.push(date); + if (firstMajorIdx == -1){ + firstMajorIdx = dates.length - 1; + } + if (i == numMajorUnits){ + break; + } + // Add minor dates + let nextMajor = stepDate(date, scale.value); + if (i < numMajorUnits){ + for (let j = 0; j < minorsPerMajor; j++){ + date = stepDate(date, minorScale.value, {count: minorTickStep}); + dates.push(date); + } + } + date = nextMajor; } endIdx = dates.length - 1; - // Get hidden following ticks - if (date.isEarlier(MAX_DATE, scale.value)){ - for (let i = 0; i < panUnits; i++){ - date = stepDate(date, scale.value); + // Get after-endDate ticks (including end-offset ticks and hidden ticks) + for (let i = 0; i < panUnits + Math.ceil(endOffset.value); i++){ + if (MAX_DATE.equals(date, scale.value)){ + break; + } + let nextMajor = stepDate(date, scale.value); + // Add minor dates + for (let j = 0; j < minorsPerMajor; j++){ + date = stepDate(date, minorScale.value, {count: minorTickStep}); dates.push(date); - if (MAX_DATE.equals(date, scale.value)){ - break; - } } + // Add major date + date = nextMajor; + dates.push(date); } // Get hidden ticks that might transition in after zooming let datesBefore: HistDate[] = []; let datesAfter: HistDate[] = []; if (scaleIdx.value > 0 && availLen.value / (numUnits * store.zoomRatio) < store.minTickSep){ // If zoom-out would decrease scale - let zoomUnits = numUnits * (store.zoomRatio - 1); // Potential distance shifted upon a zoom-out + let zoomUnits = numUnits * (store.zoomRatio - 1); // Potential shift distance upon a zoom-out if (zoomUnits > panUnits){ - let zoomedScale = SCALES[scaleIdx.value-1]; + let zoomedScale = SCALES[scaleIdx.value - 1]; let unitsPerZoomedUnit = getScaleRatio(scale.value, zoomedScale); date = dates[0]; // Get preceding ticks @@ -281,7 +354,8 @@ const ticks = computed((): Ticks => { dates = [...datesBefore, ...dates, ...datesAfter]; startIdx += datesBefore.length; endIdx += datesBefore.length; - return {dates, startIdx, endIdx}; + firstMajorIdx += datesBefore.length; + return {dates, startIdx, endIdx, firstMajorIdx, minorsPerMajor, minorTickStep}; }); // For displayed events @@ -343,7 +417,7 @@ const idToPos = computed(() => { // Note: Placing popular events first so the layout is more stable between event requests let orderedEvents = [...idToEvent.value.values()]; orderedEvents.sort((x, y) => y.pop - x.pop); - let numUnits = getNumVisibleUnits(); + let numUnits = getNumDisplayUnits(); for (let event of orderedEvents){ // Get preferred pixel offset in column let unitOffset = getUnitDiff(event.start, startDate.value, scale.value) + startOffset.value; @@ -472,7 +546,7 @@ const eventLines: ShallowRef<Map<number, LineCoords>> = shallowRef(new Map()); // Maps event ID to event line data (x, y, length, and angle) watchEffect(() => { // Used instead of computed() in order to access old values let newEventLines: Map<number, LineCoords> = new Map(); - let numUnits = getNumVisibleUnits(); + let numUnits = getNumDisplayUnits(); for (let [id, [eventX, eventY, eventW, eventH]] of idToPos.value){ let x: number; // For line end on mainline let y: number; @@ -515,7 +589,7 @@ watchEffect(() => { // Used instead of computed() in order to access old values // For panning/zooming function panTimeline(scrollRatio: number){ - let numUnits = getNumVisibleUnits(); + let numUnits = getNumDisplayUnits(); let chgUnits = numUnits * scrollRatio; let newStart = startDate.value.clone(); let newEnd = endDate.value.clone(); @@ -620,7 +694,7 @@ function zoomTimeline(zoomRatio: number){ console.log('Reached upper scale limit'); return; } - let numUnits = getNumVisibleUnits(); + let numUnits = getNumDisplayUnits(); let newNumUnits = numUnits * zoomRatio; // Get tentative bound changes let startChg: number; @@ -928,15 +1002,23 @@ const mainlineStyles = computed(() => { transitionTimingFunction: 'ease-out', }; }); -function tickStyles(idx: number){ - let offset = - (idx - ticks.value.startIdx + startOffset.value) / - (ticks.value.endIdx - ticks.value.startIdx + startOffset.value + endOffset.value) * availLen.value; - let scaleFactor = 1; - if (scaleIdx.value > 0 && - inDateScale(ticks.value.dates[idx], SCALES[scaleIdx.value-1])){ // If tick exists on larger scale - scaleFactor = store.largeTickLen / store.tickLen; +function getTickPxOffset(idx: number): [number, boolean] { // Return major-axis offset, and true if minor tick + // TODO: Replace with something cleaner + let numMajorTicks = + Math.ceil((ticks.value.endIdx - ticks.value.firstMajorIdx + 1) / (ticks.value.minorsPerMajor + 1)); + let majorTickIdxOffset = moduloPositive((idx - ticks.value.firstMajorIdx), (ticks.value.minorsPerMajor + 1)); + let majorTickIdx = (idx - ticks.value.firstMajorIdx) - majorTickIdxOffset; + let unitOffset = majorTickIdx / (ticks.value.minorsPerMajor + 1) + startOffset.value; + if (majorTickIdxOffset > 0){ + let minorUnitsPerMajor = getScaleRatio(minorScale.value, scale.value, true); + unitOffset += majorTickIdxOffset * (ticks.value.minorTickStep / minorUnitsPerMajor); } + let pxOffset = unitOffset / (numMajorTicks - 1 + startOffset.value + endOffset.value) * availLen.value; + return [pxOffset, majorTickIdxOffset > 0]; +} +function tickStyles(idx: number){ + let [offset, isMinor] = getTickPxOffset(idx); + let scaleFactor = isMinor ? 1 : store.largeTickLen / store.tickLen; return { transform: props.vert ? `translate(${mainlineOffset.value}px, ${offset}px) scale(${scaleFactor})` : @@ -948,18 +1030,17 @@ function tickStyles(idx: number){ } } function tickLabelStyles(idx: number){ - let offset = - (idx - ticks.value.startIdx + startOffset.value) / - (ticks.value.endIdx - ticks.value.startIdx + startOffset.value + endOffset.value) * availLen.value; + let [offset, isMinor] = getTickPxOffset(idx); + let offset2 = isMinor ? 0 : 20; let labelSz = props.vert ? store.tickLabelHeight : tickLabelWidth.value; return { transform: props.vert ? - `translate(${mainlineOffset.value + tickLabelMargin.value}px, ${offset}px)` : - `translate(${offset}px, ${mainlineOffset.value + tickLabelMargin.value}px)`, + `translate(${mainlineOffset.value + tickLabelMargin.value + offset2}px, ${offset}px)` : + `translate(${offset}px, ${mainlineOffset.value + tickLabelMargin.value + offset2}px)`, transitionProperty: skipTransition.value ? 'none' : 'transform, opacity', transitionDuration: store.transitionDuration + 'ms', transitionTimingFunction: 'ease-out', - opacity: (offset >= labelSz && offset <= availLen.value - labelSz) ? 1 : 0, + display: (offset >= labelSz && offset <= availLen.value - labelSz) ? 'block' : 'none', } } function eventStyles(eventId: number){ |
