UNPKG

@29aries/apexcharts

Version:

Interactive JavaScript Charts built on SVG - forked by 29aries to allow custom labels - forked from 3.20.0

801 lines (696 loc) 21.4 kB
import DateTime from '../utils/DateTime' import Dimensions from './dimensions/Dimensions' import Graphics from './Graphics' import Utils from '../utils/Utils' /** * ApexCharts TimeScale Class for generating time ticks for x-axis. * * @module TimeScale **/ class TimeScale { constructor(ctx) { this.ctx = ctx this.w = ctx.w this.timeScaleArray = [] this.utc = this.w.config.xaxis.labels.datetimeUTC } calculateTimeScaleTicks(minX, maxX) { let w = this.w // null check when no series to show if (w.globals.allSeriesCollapsed) { w.globals.labels = [] w.globals.timescaleLabels = [] return [] } let dt = new DateTime(this.ctx) const daysDiff = (maxX - minX) / (1000 * 60 * 60 * 24) this.determineInterval(daysDiff) w.globals.disableZoomIn = false w.globals.disableZoomOut = false if (daysDiff < 0.005) { w.globals.disableZoomIn = true } else if (daysDiff > 50000) { w.globals.disableZoomOut = true } const timeIntervals = dt.getTimeUnitsfromTimestamp(minX, maxX, this.utc) const daysWidthOnXAxis = w.globals.gridWidth / daysDiff const hoursWidthOnXAxis = daysWidthOnXAxis / 24 const minutesWidthOnXAxis = hoursWidthOnXAxis / 60 let numberOfHours = Math.floor(daysDiff * 24) let numberOfMinutes = Math.floor(daysDiff * 24 * 60) let numberOfDays = Math.floor(daysDiff) let numberOfMonths = Math.floor(daysDiff / 30) let numberOfYears = Math.floor(daysDiff / 365) const firstVal = { minMinute: timeIntervals.minMinute, minHour: timeIntervals.minHour, minDate: timeIntervals.minDate, minMonth: timeIntervals.minMonth, minYear: timeIntervals.minYear } let currentMinute = firstVal.minMinute let currentHour = firstVal.minHour let currentMonthDate = firstVal.minDate let currentDate = firstVal.minDate let currentMonth = firstVal.minMonth let currentYear = firstVal.minYear const params = { firstVal, currentMinute, currentHour, currentMonthDate, currentDate, currentMonth, currentYear, daysWidthOnXAxis, hoursWidthOnXAxis, minutesWidthOnXAxis, numberOfMinutes, numberOfHours, numberOfDays, numberOfMonths, numberOfYears } switch (this.tickInterval) { case 'years': { this.generateYearScale(params) break } case 'months': case 'half_year': { this.generateMonthScale(params) break } case 'months_days': case 'months_fortnight': case 'days': case 'week_days': { this.generateDayScale(params) break } case 'hours': { this.generateHourScale(params) break } case 'minutes': this.generateMinuteScale(params) break } // first, we will adjust the month values index // as in the upper function, it is starting from 0 // we will start them from 1 const adjustedMonthInTimeScaleArray = this.timeScaleArray.map((ts) => { let defaultReturn = { position: ts.position, unit: ts.unit, year: ts.year, day: ts.day ? ts.day : 1, hour: ts.hour ? ts.hour : 0, month: ts.month + 1 } if (ts.unit === 'month') { return { ...defaultReturn, day: 1, value: ts.value + 1 } } else if (ts.unit === 'day' || ts.unit === 'hour') { return { ...defaultReturn, value: ts.value } } else if (ts.unit === 'minute') { return { ...defaultReturn, value: ts.value, minute: ts.value } } return ts }) const filteredTimeScale = adjustedMonthInTimeScaleArray.filter((ts) => { let modulo = 1 let ticks = Math.ceil(w.globals.gridWidth / 120) let value = ts.value if (w.config.xaxis.tickAmount !== undefined) { ticks = w.config.xaxis.tickAmount } if (adjustedMonthInTimeScaleArray.length > ticks) { modulo = Math.floor(adjustedMonthInTimeScaleArray.length / ticks) } let shouldNotSkipUnit = false // there is a big change in unit i.e days to months let shouldNotPrint = false // should skip these values switch (this.tickInterval) { case 'years': // make years label denser if (ts.unit === 'year') { shouldNotSkipUnit = true } break case 'half_year': modulo = 7 if (ts.unit === 'year') { shouldNotSkipUnit = true } break case 'months': modulo = 1 if (ts.unit === 'year') { shouldNotSkipUnit = true } break case 'months_fortnight': modulo = 15 if (ts.unit === 'year' || ts.unit === 'month') { shouldNotSkipUnit = true } if (value === 30) { shouldNotPrint = true } break case 'months_days': modulo = 10 if (ts.unit === 'month') { shouldNotSkipUnit = true } if (value === 30) { shouldNotPrint = true } break case 'week_days': modulo = 8 if (ts.unit === 'month') { shouldNotSkipUnit = true } break case 'days': modulo = 1 if (ts.unit === 'month') { shouldNotSkipUnit = true } break case 'hours': if (ts.unit === 'day') { shouldNotSkipUnit = true } break case 'minutes': if (value % 5 !== 0) { shouldNotPrint = true } break } if (this.tickInterval === 'minutes' || this.tickInterval === 'hours') { if (!shouldNotPrint) { return true } } else { if ((value % modulo === 0 || shouldNotSkipUnit) && !shouldNotPrint) { return true } } }) return filteredTimeScale } recalcDimensionsBasedOnFormat(filteredTimeScale, inverted) { const w = this.w const reformattedTimescaleArray = this.formatDates(filteredTimeScale) const removedOverlappingTS = this.removeOverlappingTS( reformattedTimescaleArray ) w.globals.timescaleLabels = removedOverlappingTS.slice() // at this stage, we need to re-calculate coords of the grid as timeline labels may have altered the xaxis labels coords // The reason we can't do this prior to this stage is because timeline labels depends on gridWidth, and as the ticks are calculated based on available gridWidth, there can be unknown number of ticks generated for different minX and maxX // Dependency on Dimensions(), need to refactor correctly // TODO - find an alternate way to avoid calling this Heavy method twice let dimensions = new Dimensions(this.ctx) dimensions.plotCoords() } determineInterval(daysDiff) { switch (true) { case daysDiff > 1825: // difference is more than 5 years this.tickInterval = 'years' break case daysDiff > 800 && daysDiff <= 1825: this.tickInterval = 'half_year' break case daysDiff > 180 && daysDiff <= 800: this.tickInterval = 'months' break case daysDiff > 90 && daysDiff <= 180: this.tickInterval = 'months_fortnight' break case daysDiff > 60 && daysDiff <= 90: this.tickInterval = 'months_days' break case daysDiff > 30 && daysDiff <= 60: this.tickInterval = 'week_days' break case daysDiff > 2 && daysDiff <= 30: this.tickInterval = 'days' break case daysDiff > 0.1 && daysDiff <= 2: // less than 2 days this.tickInterval = 'hours' break case daysDiff < 0.1: this.tickInterval = 'minutes' break default: this.tickInterval = 'days' break } } generateYearScale({ firstVal, currentMonth, currentYear, daysWidthOnXAxis, numberOfYears }) { let firstTickValue = firstVal.minYear let firstTickPosition = 0 const dt = new DateTime(this.ctx) let unit = 'year' if (firstVal.minDate > 1 || firstVal.minMonth > 0) { let remainingDays = dt.determineRemainingDaysOfYear( firstVal.minYear, firstVal.minMonth, firstVal.minDate ) // remainingDaysofFirstMonth is used to reacht the 2nd tick position let remainingDaysOfFirstYear = dt.determineDaysOfYear(firstVal.minYear) - remainingDays + 1 // calculate the first tick position firstTickPosition = remainingDaysOfFirstYear * daysWidthOnXAxis firstTickValue = firstVal.minYear + 1 // push the first tick in the array this.timeScaleArray.push({ position: firstTickPosition, value: firstTickValue, unit, year: firstTickValue, month: Utils.monthMod(currentMonth + 1) }) } else if (firstVal.minDate === 1 && firstVal.minMonth === 0) { // push the first tick in the array this.timeScaleArray.push({ position: firstTickPosition, value: firstTickValue, unit, year: currentYear, month: Utils.monthMod(currentMonth + 1) }) } let year = firstTickValue let pos = firstTickPosition // keep drawing rest of the ticks for (let i = 0; i < numberOfYears; i++) { year++ pos = dt.determineDaysOfYear(year - 1) * daysWidthOnXAxis + pos this.timeScaleArray.push({ position: pos, value: year, unit, year, month: 1 }) } } generateMonthScale({ firstVal, currentMonthDate, currentMonth, currentYear, daysWidthOnXAxis, numberOfMonths }) { let firstTickValue = currentMonth let firstTickPosition = 0 const dt = new DateTime(this.ctx) let unit = 'month' let yrCounter = 0 if (firstVal.minDate > 1) { // remainingDaysofFirstMonth is used to reacht the 2nd tick position let remainingDaysOfFirstMonth = dt.determineDaysOfMonths(currentMonth + 1, firstVal.minYear) - currentMonthDate + 1 // calculate the first tick position firstTickPosition = remainingDaysOfFirstMonth * daysWidthOnXAxis firstTickValue = Utils.monthMod(currentMonth + 1) let year = currentYear + yrCounter let month = Utils.monthMod(firstTickValue) let value = firstTickValue // it's Jan, so update the year if (firstTickValue === 0) { unit = 'year' value = year month = 1 yrCounter += 1 year = year + yrCounter } // push the first tick in the array this.timeScaleArray.push({ position: firstTickPosition, value, unit, year, month }) } else { // push the first tick in the array this.timeScaleArray.push({ position: firstTickPosition, value: firstTickValue, unit, year: currentYear, month: Utils.monthMod(currentMonth) }) } let month = firstTickValue + 1 let pos = firstTickPosition // keep drawing rest of the ticks for (let i = 0, j = 1; i < numberOfMonths; i++, j++) { month = Utils.monthMod(month) if (month === 0) { unit = 'year' yrCounter += 1 } else { unit = 'month' } let year = this._getYear(currentYear, month, yrCounter) pos = dt.determineDaysOfMonths(month, year) * daysWidthOnXAxis + pos let monthVal = month === 0 ? year : month this.timeScaleArray.push({ position: pos, value: monthVal, unit, year, month: month === 0 ? 1 : month }) month++ } } generateDayScale({ firstVal, currentMonth, currentYear, hoursWidthOnXAxis, numberOfDays }) { const dt = new DateTime(this.ctx) let unit = 'day' let firstTickValue = firstVal.minDate + 1 let date = firstTickValue const changeMonth = (dateVal, month, year) => { let monthdays = dt.determineDaysOfMonths(month + 1, year) if (dateVal > monthdays) { month = month + 1 date = 1 unit = 'month' val = month return month } return month } let remainingHours = 24 - firstVal.minHour let yrCounter = 0 // calculate the first tick position let firstTickPosition = remainingHours * hoursWidthOnXAxis let val = firstTickValue let month = changeMonth(date, currentMonth, currentYear) if (firstVal.minHour === 0 && firstVal.minDate === 1) { // the first value is the first day of month firstTickPosition = 0 val = Utils.monthMod(firstVal.minMonth) unit = 'month' date = firstVal.minDate numberOfDays++ } // push the first tick in the array this.timeScaleArray.push({ position: firstTickPosition, value: val, unit, year: this._getYear(currentYear, month, yrCounter), month: Utils.monthMod(month), day: date }) let pos = firstTickPosition // keep drawing rest of the ticks for (let i = 0; i < numberOfDays; i++) { date += 1 unit = 'day' month = changeMonth( date, month, this._getYear(currentYear, month, yrCounter) ) let year = this._getYear(currentYear, month, yrCounter) pos = 24 * hoursWidthOnXAxis + pos let value = date === 1 ? Utils.monthMod(month) : date this.timeScaleArray.push({ position: pos, value, unit, year, month: Utils.monthMod(month), day: value }) } } generateHourScale({ firstVal, currentDate, currentMonth, currentYear, minutesWidthOnXAxis, numberOfHours }) { const dt = new DateTime(this.ctx) let yrCounter = 0 let unit = 'hour' const changeDate = (dateVal, month) => { let monthdays = dt.determineDaysOfMonths(month + 1, currentYear) if (dateVal > monthdays) { date = 1 month = month + 1 } return { month, date } } const changeMonth = (dateVal, month) => { let monthdays = dt.determineDaysOfMonths(month + 1, currentYear) if (dateVal > monthdays) { month = month + 1 return month } return month } let remainingMins = 60 - firstVal.minMinute let firstTickPosition = remainingMins * minutesWidthOnXAxis let firstTickValue = firstVal.minHour + 1 let hour = firstTickValue + 1 if (remainingMins === 60) { firstTickPosition = 0 firstTickValue = firstVal.minHour hour = firstTickValue + 1 } let date = currentDate let month = changeMonth(date, currentMonth) // push the first tick in the array this.timeScaleArray.push({ position: firstTickPosition, value: firstTickValue, unit, day: date, hour, year: currentYear, month: Utils.monthMod(month) }) let pos = firstTickPosition // keep drawing rest of the ticks for (let i = 0; i < numberOfHours; i++) { unit = 'hour' if (hour >= 24) { hour = 0 date += 1 unit = 'day' const checkNextMonth = changeDate(date, month) month = checkNextMonth.month month = changeMonth(date, month) } let year = this._getYear(currentYear, month, yrCounter) pos = hour === 0 && i === 0 ? remainingMins * minutesWidthOnXAxis : 60 * minutesWidthOnXAxis + pos let val = hour === 0 ? date : hour this.timeScaleArray.push({ position: pos, value: val, unit, hour, day: date, year, month: Utils.monthMod(month) }) hour++ } } generateMinuteScale({ firstVal, currentMinute, currentHour, currentDate, currentMonth, currentYear, minutesWidthOnXAxis, numberOfMinutes }) { let yrCounter = 0 let unit = 'minute' let remainingMins = currentMinute - firstVal.minMinute let firstTickPosition = minutesWidthOnXAxis - remainingMins let firstTickValue = firstVal.minMinute + 1 let minute = firstTickValue + 1 let date = currentDate let month = currentMonth let year = currentYear let hour = currentHour // push the first tick in the array this.timeScaleArray.push({ position: firstTickPosition, value: firstTickValue, unit, day: date, hour, minute, year, month: Utils.monthMod(month) }) let pos = firstTickPosition // keep drawing rest of the ticks for (let i = 0; i < numberOfMinutes; i++) { if (minute >= 60) { minute = 0 hour += 1 if (hour === 24) { hour = 0 } } pos = minutesWidthOnXAxis + pos this.timeScaleArray.push({ position: pos, value: minute, unit, hour, minute, day: date, year: this._getYear(currentYear, month, yrCounter), month: Utils.monthMod(month) }) minute++ } } createRawDateString(ts, value) { let raw = ts.year raw += '-' + ('0' + ts.month.toString()).slice(-2) // unit is day if (ts.unit === 'day') { raw += ts.unit === 'day' ? '-' + ('0' + value).slice(-2) : '-01' } else { raw += '-' + ('0' + (ts.day ? ts.day : '1')).slice(-2) } // unit is hour if (ts.unit === 'hour') { raw += ts.unit === 'hour' ? 'T' + ('0' + value).slice(-2) : 'T00' } else { raw += 'T' + ('0' + (ts.hour ? ts.hour : '0')).slice(-2) } // unit is minute raw += ts.unit === 'minute' ? ':' + ('0' + value).slice(-2) + ':00' : ':00:00' if (this.utc) { raw += '.000Z' } return raw } formatDates(filteredTimeScale) { const w = this.w const reformattedTimescaleArray = filteredTimeScale.map((ts) => { let value = ts.value.toString() let dt = new DateTime(this.ctx) const raw = this.createRawDateString(ts, value) let dateToFormat = dt.getDate(dt.parseDate(raw)) if (!this.utc) { // Fixes #1726, #1544, #1485, #1255 dateToFormat = dt.getDate(dt.parseDateWithTimezone(raw)) } if (w.config.xaxis.labels.format === undefined) { let customFormat = 'dd MMM' let customSufix = '' const dtFormatter = w.config.xaxis.labels.datetimeFormatter if (ts.unit === 'year') customFormat = dtFormatter.year if (ts.unit === 'month') customFormat = dtFormatter.month if (ts.unit === 'day') customFormat = dtFormatter.day if (ts.unit === 'hour') customFormat = dtFormatter.hour if (ts.unit === 'minute') customFormat = dtFormatter.minute const dtSufix = w.config.xaxis.labels.datetimeSufix if (ts.unit === 'year') customSufix = dtSufix.year if (ts.unit === 'month') customSufix = dtSufix.month if (ts.unit === 'day') customSufix = dtSufix.day if (ts.unit === 'hour') customSufix = dtSufix.hour if (ts.unit === 'minute') customSufix = dtSufix.minute value = dt.formatDate(dateToFormat, customFormat) + customSufix } else { value = dt.formatDate(dateToFormat, w.config.xaxis.labels.format) } return { dateString: raw, position: ts.position, value, unit: ts.unit, year: ts.year, month: ts.month } }) return reformattedTimescaleArray } removeOverlappingTS(arr) { const graphics = new Graphics(this.ctx) let equalLabelLengthFlag = false // These labels got same length? let constantLabelWidth // If true, what is the constant length to use if ( arr.length > 0 && // check arr length arr[0].value && // check arr[0] contains value arr.every((lb) => lb.value.length === arr[0].value.length) // check every arr label value is the same as the first one ) { equalLabelLengthFlag = true // These labels got same length constantLabelWidth = graphics.getTextRects(arr[0].value).width // The constant label width to use } let lastDrawnIndex = 0 let filteredArray = arr.map((item, index) => { if (index > 0 && this.w.config.xaxis.labels.hideOverlappingLabels) { const prevLabelWidth = !equalLabelLengthFlag // if vary in label length ? graphics.getTextRects(arr[lastDrawnIndex].value).width // get individual length : constantLabelWidth // else: use constant length const prevPos = arr[lastDrawnIndex].position const pos = item.position if (pos > prevPos + prevLabelWidth + 10) { lastDrawnIndex = index return item } else { return null } } else { return item } }) filteredArray = filteredArray.filter((f) => f !== null) return filteredArray } _getYear(currentYear, month, yrCounter) { return currentYear + Math.floor(month / 12) + yrCounter } } export default TimeScale