| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import * as zrUtil from 'zrender/src/core/util'; |
| import * as layout from '../../util/layout'; |
| import * as numberUtil from '../../util/number'; |
| import BoundingRect, {RectLike} from 'zrender/src/core/BoundingRect'; |
| import CalendarModel from './CalendarModel'; |
| import GlobalModel from '../../model/Global'; |
| import ExtensionAPI from '../../core/ExtensionAPI'; |
| import { |
| LayoutOrient, |
| ScaleDataValue, |
| OptionDataValueDate, |
| SeriesOption, |
| SeriesOnCalendarOptionMixin |
| } from '../../util/types'; |
| import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; |
| import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; |
| import SeriesModel from '../../model/Series'; |
| |
| // (24*60*60*1000) |
| const PROXIMATE_ONE_DAY = 86400000; |
| |
| |
| export interface CalendarParsedDateRangeInfo { |
| range: [string, string], |
| start: CalendarParsedDateInfo |
| end: CalendarParsedDateInfo |
| allDay: number |
| weeks: number |
| nthWeek: number |
| fweek: number |
| lweek: number |
| } |
| |
| export interface CalendarParsedDateInfo { |
| /** |
| * local full year, eg., '1940' |
| */ |
| y: string |
| /** |
| * local month, from '01' ot '12', |
| */ |
| m: string |
| /** |
| * local date, from '01' to '31' (if exists), |
| */ |
| d: string |
| /** |
| * It is not date.getDay(). It is the location of the cell in a week, from 0 to 6, |
| */ |
| day: number |
| /** |
| * Timestamp |
| */ |
| time: number |
| /** |
| * yyyy-MM-dd |
| */ |
| formatedDate: string |
| /** |
| * The original date object |
| */ |
| date: Date |
| } |
| |
| export interface CalendarCellRect { |
| contentShape: RectLike |
| center: number[] |
| tl: number[] |
| tr: number[] |
| br: number[] |
| bl: number[] |
| } |
| |
| class Calendar implements CoordinateSystem, CoordinateSystemMaster { |
| |
| static readonly dimensions = ['time', 'value']; |
| static getDimensionsInfo() { |
| return [{ |
| name: 'time', type: 'time' as const |
| }, 'value']; |
| } |
| |
| readonly type = 'calendar'; |
| |
| readonly dimensions = Calendar.dimensions; |
| |
| private _model: CalendarModel; |
| |
| private _rect: BoundingRect; |
| |
| private _sw: number; |
| private _sh: number; |
| private _orient: LayoutOrient; |
| |
| private _firstDayOfWeek: number; |
| |
| private _rangeInfo: CalendarParsedDateRangeInfo; |
| |
| private _lineWidth: number; |
| |
| constructor(calendarModel: CalendarModel, ecModel: GlobalModel, api: ExtensionAPI) { |
| this._model = calendarModel; |
| } |
| // Required in createListFromData |
| getDimensionsInfo = Calendar.getDimensionsInfo; |
| |
| getRangeInfo() { |
| return this._rangeInfo; |
| } |
| |
| getModel() { |
| return this._model; |
| } |
| |
| getRect() { |
| return this._rect; |
| } |
| |
| getCellWidth() { |
| return this._sw; |
| } |
| |
| getCellHeight() { |
| return this._sh; |
| } |
| |
| getOrient() { |
| return this._orient; |
| } |
| |
| /** |
| * getFirstDayOfWeek |
| * |
| * @example |
| * 0 : start at Sunday |
| * 1 : start at Monday |
| * |
| * @return {number} |
| */ |
| getFirstDayOfWeek() { |
| return this._firstDayOfWeek; |
| } |
| |
| /** |
| * get date info |
| * } |
| */ |
| getDateInfo(date: OptionDataValueDate): CalendarParsedDateInfo { |
| |
| date = numberUtil.parseDate(date); |
| |
| const y = date.getFullYear(); |
| |
| const m = date.getMonth() + 1; |
| const mStr = m < 10 ? '0' + m : '' + m; |
| |
| const d = date.getDate(); |
| const dStr = d < 10 ? '0' + d : '' + d; |
| |
| let day = date.getDay(); |
| |
| day = Math.abs((day + 7 - this.getFirstDayOfWeek()) % 7); |
| |
| return { |
| y: y + '', |
| m: mStr, |
| d: dStr, |
| day: day, |
| time: date.getTime(), |
| formatedDate: y + '-' + mStr + '-' + dStr, |
| date: date |
| }; |
| } |
| |
| getNextNDay(date: OptionDataValueDate, n: number) { |
| n = n || 0; |
| if (n === 0) { |
| return this.getDateInfo(date); |
| } |
| |
| date = new Date(this.getDateInfo(date).time); |
| date.setDate(date.getDate() + n); |
| |
| return this.getDateInfo(date); |
| } |
| |
| update(ecModel: GlobalModel, api: ExtensionAPI) { |
| |
| this._firstDayOfWeek = +this._model.getModel('dayLabel').get('firstDay'); |
| this._orient = this._model.get('orient'); |
| this._lineWidth = this._model.getModel('itemStyle').getItemStyle().lineWidth || 0; |
| |
| |
| this._rangeInfo = this._getRangeInfo(this._initRangeOption()); |
| const weeks = this._rangeInfo.weeks || 1; |
| const whNames = ['width', 'height'] as const; |
| const cellSize = this._model.getCellSize().slice(); |
| const layoutParams = this._model.getBoxLayoutParams(); |
| const cellNumbers = this._orient === 'horizontal' ? [weeks, 7] : [7, weeks]; |
| |
| zrUtil.each([0, 1] as const, function (idx) { |
| if (cellSizeSpecified(cellSize, idx)) { |
| layoutParams[whNames[idx]] = cellSize[idx] * cellNumbers[idx]; |
| } |
| }); |
| |
| const whGlobal = { |
| width: api.getWidth(), |
| height: api.getHeight() |
| }; |
| const calendarRect = this._rect = layout.getLayoutRect(layoutParams, whGlobal); |
| |
| zrUtil.each([0, 1], function (idx) { |
| if (!cellSizeSpecified(cellSize, idx)) { |
| cellSize[idx] = calendarRect[whNames[idx]] / cellNumbers[idx]; |
| } |
| }); |
| |
| function cellSizeSpecified(cellSize: (number | 'auto')[], idx: number): cellSize is number[] { |
| return cellSize[idx] != null && cellSize[idx] !== 'auto'; |
| } |
| |
| // Has been calculated out number. |
| this._sw = cellSize[0] as number; |
| this._sh = cellSize[1] as number; |
| } |
| |
| |
| /** |
| * Convert a time data(time, value) item to (x, y) point. |
| */ |
| // TODO Clamp of calendar is not same with cartesian coordinate systems. |
| // It will return NaN if data exceeds. |
| dataToPoint(data: OptionDataValueDate | OptionDataValueDate[], clamp?: boolean) { |
| zrUtil.isArray(data) && (data = data[0]); |
| clamp == null && (clamp = true); |
| |
| const dayInfo = this.getDateInfo(data); |
| const range = this._rangeInfo; |
| const date = dayInfo.formatedDate; |
| |
| // if not in range return [NaN, NaN] |
| if (clamp && !( |
| dayInfo.time >= range.start.time |
| && dayInfo.time < range.end.time + PROXIMATE_ONE_DAY |
| )) { |
| return [NaN, NaN]; |
| } |
| |
| const week = dayInfo.day; |
| const nthWeek = this._getRangeInfo([range.start.time, date]).nthWeek; |
| |
| if (this._orient === 'vertical') { |
| return [ |
| this._rect.x + week * this._sw + this._sw / 2, |
| this._rect.y + nthWeek * this._sh + this._sh / 2 |
| ]; |
| |
| } |
| |
| return [ |
| this._rect.x + nthWeek * this._sw + this._sw / 2, |
| this._rect.y + week * this._sh + this._sh / 2 |
| ]; |
| |
| } |
| |
| /** |
| * Convert a (x, y) point to time data |
| */ |
| pointToData(point: number[]): number { |
| |
| const date = this.pointToDate(point); |
| |
| return date && date.time; |
| } |
| |
| /** |
| * Convert a time date item to (x, y) four point. |
| */ |
| dataToRect(data: OptionDataValueDate | OptionDataValueDate[], clamp?: boolean): CalendarCellRect { |
| const point = this.dataToPoint(data, clamp); |
| |
| return { |
| contentShape: { |
| x: point[0] - (this._sw - this._lineWidth) / 2, |
| y: point[1] - (this._sh - this._lineWidth) / 2, |
| width: this._sw - this._lineWidth, |
| height: this._sh - this._lineWidth |
| }, |
| |
| center: point, |
| |
| tl: [ |
| point[0] - this._sw / 2, |
| point[1] - this._sh / 2 |
| ], |
| |
| tr: [ |
| point[0] + this._sw / 2, |
| point[1] - this._sh / 2 |
| ], |
| |
| br: [ |
| point[0] + this._sw / 2, |
| point[1] + this._sh / 2 |
| ], |
| |
| bl: [ |
| point[0] - this._sw / 2, |
| point[1] + this._sh / 2 |
| ] |
| |
| }; |
| } |
| |
| /** |
| * Convert a (x, y) point to time date |
| * |
| * @param {Array} point point |
| * @return {Object} date |
| */ |
| pointToDate(point: number[]): CalendarParsedDateInfo { |
| const nthX = Math.floor((point[0] - this._rect.x) / this._sw) + 1; |
| const nthY = Math.floor((point[1] - this._rect.y) / this._sh) + 1; |
| const range = this._rangeInfo.range; |
| |
| if (this._orient === 'vertical') { |
| return this._getDateByWeeksAndDay(nthY, nthX - 1, range); |
| } |
| |
| return this._getDateByWeeksAndDay(nthX, nthY - 1, range); |
| } |
| |
| convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue | ScaleDataValue[]) { |
| const coordSys = getCoordSys(finder); |
| return coordSys === this ? coordSys.dataToPoint(value) : null; |
| } |
| |
| convertFromPixel(ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]) { |
| const coordSys = getCoordSys(finder); |
| return coordSys === this ? coordSys.pointToData(pixel) : null; |
| } |
| |
| containPoint(point: number[]): boolean { |
| console.warn('Not implemented.'); |
| return false; |
| } |
| |
| /** |
| * initRange |
| * Normalize to an [start, end] array |
| */ |
| private _initRangeOption(): OptionDataValueDate[] { |
| let range = this._model.get('range'); |
| let normalizedRange: OptionDataValueDate[]; |
| |
| // Convert [1990] to 1990 |
| if (zrUtil.isArray(range) && range.length === 1) { |
| range = range[0]; |
| } |
| |
| if (!zrUtil.isArray(range)) { |
| const rangeStr = range.toString(); |
| // One year. |
| if (/^\d{4}$/.test(rangeStr)) { |
| normalizedRange = [rangeStr + '-01-01', rangeStr + '-12-31']; |
| } |
| // One month |
| if (/^\d{4}[\/|-]\d{1,2}$/.test(rangeStr)) { |
| |
| const start = this.getDateInfo(rangeStr); |
| const firstDay = start.date; |
| firstDay.setMonth(firstDay.getMonth() + 1); |
| |
| const end = this.getNextNDay(firstDay, -1); |
| normalizedRange = [start.formatedDate, end.formatedDate]; |
| } |
| // One day |
| if (/^\d{4}[\/|-]\d{1,2}[\/|-]\d{1,2}$/.test(rangeStr)) { |
| normalizedRange = [rangeStr, rangeStr]; |
| } |
| } |
| else { |
| normalizedRange = range; |
| } |
| |
| if (!normalizedRange) { |
| if (__DEV__) { |
| zrUtil.logError('Invalid date range.'); |
| } |
| // Not handling it. |
| return range as OptionDataValueDate[]; |
| } |
| |
| const tmp = this._getRangeInfo(normalizedRange); |
| |
| if (tmp.start.time > tmp.end.time) { |
| normalizedRange.reverse(); |
| } |
| |
| return normalizedRange; |
| } |
| |
| /** |
| * range info |
| * |
| * @private |
| * @param {Array} range range ['2017-01-01', '2017-07-08'] |
| * If range[0] > range[1], they will not be reversed. |
| * @return {Object} obj |
| */ |
| _getRangeInfo(range: OptionDataValueDate[]): CalendarParsedDateRangeInfo { |
| const parsedRange = [ |
| this.getDateInfo(range[0]), |
| this.getDateInfo(range[1]) |
| ]; |
| |
| let reversed; |
| if (parsedRange[0].time > parsedRange[1].time) { |
| reversed = true; |
| parsedRange.reverse(); |
| } |
| |
| let allDay = Math.floor(parsedRange[1].time / PROXIMATE_ONE_DAY) |
| - Math.floor(parsedRange[0].time / PROXIMATE_ONE_DAY) + 1; |
| |
| // Consider case1 (#11677 #10430): |
| // Set the system timezone as "UK", set the range to `['2016-07-01', '2016-12-31']` |
| |
| // Consider case2: |
| // Firstly set system timezone as "Time Zone: America/Toronto", |
| // ``` |
| // let first = new Date(1478412000000 - 3600 * 1000 * 2.5); |
| // let second = new Date(1478412000000); |
| // let allDays = Math.floor(second / ONE_DAY) - Math.floor(first / ONE_DAY) + 1; |
| // ``` |
| // will get wrong result because of DST. So we should fix it. |
| const date = new Date(parsedRange[0].time); |
| const startDateNum = date.getDate(); |
| const endDateNum = parsedRange[1].date.getDate(); |
| date.setDate(startDateNum + allDay - 1); |
| // The bias can not over a month, so just compare date. |
| let dateNum = date.getDate(); |
| if (dateNum !== endDateNum) { |
| const sign = date.getTime() - parsedRange[1].time > 0 ? 1 : -1; |
| while ( |
| (dateNum = date.getDate()) !== endDateNum |
| && (date.getTime() - parsedRange[1].time) * sign > 0 |
| ) { |
| allDay -= sign; |
| date.setDate(dateNum - sign); |
| } |
| } |
| |
| const weeks = Math.floor((allDay + parsedRange[0].day + 6) / 7); |
| const nthWeek = reversed ? -weeks + 1 : weeks - 1; |
| |
| reversed && parsedRange.reverse(); |
| |
| return { |
| range: [parsedRange[0].formatedDate, parsedRange[1].formatedDate], |
| start: parsedRange[0], |
| end: parsedRange[1], |
| allDay: allDay, |
| weeks: weeks, |
| // From 0. |
| nthWeek: nthWeek, |
| fweek: parsedRange[0].day, |
| lweek: parsedRange[1].day |
| }; |
| } |
| |
| /** |
| * get date by nthWeeks and week day in range |
| * |
| * @private |
| * @param {number} nthWeek the week |
| * @param {number} day the week day |
| * @param {Array} range [d1, d2] |
| * @return {Object} |
| */ |
| private _getDateByWeeksAndDay(nthWeek: number, day: number, range: OptionDataValueDate[]): CalendarParsedDateInfo { |
| const rangeInfo = this._getRangeInfo(range); |
| |
| if (nthWeek > rangeInfo.weeks |
| || (nthWeek === 0 && day < rangeInfo.fweek) |
| || (nthWeek === rangeInfo.weeks && day > rangeInfo.lweek) |
| ) { |
| return null; |
| } |
| |
| const nthDay = (nthWeek - 1) * 7 - rangeInfo.fweek + day; |
| const date = new Date(rangeInfo.start.time); |
| date.setDate(+rangeInfo.start.d + nthDay); |
| |
| return this.getDateInfo(date); |
| } |
| |
| static create(ecModel: GlobalModel, api: ExtensionAPI) { |
| const calendarList: Calendar[] = []; |
| |
| ecModel.eachComponent('calendar', function (calendarModel: CalendarModel) { |
| const calendar = new Calendar(calendarModel, ecModel, api); |
| calendarList.push(calendar); |
| calendarModel.coordinateSystem = calendar; |
| }); |
| |
| ecModel.eachSeries(function (calendarSeries: SeriesModel<SeriesOption & SeriesOnCalendarOptionMixin>) { |
| if (calendarSeries.get('coordinateSystem') === 'calendar') { |
| // Inject coordinate system |
| calendarSeries.coordinateSystem = calendarList[calendarSeries.get('calendarIndex') || 0]; |
| } |
| }); |
| return calendarList; |
| } |
| } |
| |
| function getCoordSys(finder: ParsedModelFinderKnown): Calendar { |
| const calendarModel = finder.calendarModel as CalendarModel; |
| const seriesModel = finder.seriesModel; |
| |
| const coordSys = calendarModel |
| ? calendarModel.coordinateSystem |
| : seriesModel |
| ? seriesModel.coordinateSystem |
| : null; |
| |
| return coordSys as Calendar; |
| } |
| |
| export default Calendar; |