/* * 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 { isString, extend, map, isFunction } from 'zrender/src/core/util'; import * as graphic from '../../util/graphic'; import {createTextStyle} from '../../label/labelStyle'; import { formatTplSimple } from '../../util/format'; import { parsePercent } from '../../util/number'; import type CalendarModel from '../../coord/calendar/CalendarModel'; import {CalendarParsedDateRangeInfo, CalendarParsedDateInfo} from '../../coord/calendar/Calendar'; import type GlobalModel from '../../model/Global'; import type ExtensionAPI from '../../core/ExtensionAPI'; import { LayoutOrient, OptionDataValueDate, ZRTextAlign, ZRTextVerticalAlign } from '../../util/types'; import ComponentView from '../../view/Component'; import { PathStyleProps } from 'zrender/src/graphic/Path'; import { TextStyleProps, TextProps } from 'zrender/src/graphic/Text'; import { LocaleOption, getLocaleModel } from '../../core/locale'; import type Model from '../../model/Model'; class CalendarView extends ComponentView { static type = 'calendar'; type = CalendarView.type; /** * top/left line points */ private _tlpoints: number[][]; /** * bottom/right line points */ private _blpoints: number[][]; /** * first day of month */ private _firstDayOfMonth: CalendarParsedDateInfo[]; /** * first day point of month */ private _firstDayPoints: number[][]; render(calendarModel: CalendarModel, ecModel: GlobalModel, api: ExtensionAPI) { const group = this.group; group.removeAll(); const coordSys = calendarModel.coordinateSystem; // range info const rangeData = coordSys.getRangeInfo(); const orient = coordSys.getOrient(); // locale const localeModel = ecModel.getLocaleModel(); this._renderDayRect(calendarModel, rangeData, group); // _renderLines must be called prior to following function this._renderLines(calendarModel, rangeData, orient, group); this._renderYearText(calendarModel, rangeData, orient, group); this._renderMonthText(calendarModel, localeModel, orient, group); this._renderWeekText(calendarModel, localeModel, rangeData, orient, group); } // render day rect _renderDayRect(calendarModel: CalendarModel, rangeData: CalendarParsedDateRangeInfo, group: graphic.Group) { const coordSys = calendarModel.coordinateSystem; const itemRectStyleModel = calendarModel.getModel('itemStyle').getItemStyle(); const sw = coordSys.getCellWidth(); const sh = coordSys.getCellHeight(); for (let i = rangeData.start.time; i <= rangeData.end.time; i = coordSys.getNextNDay(i, 1).time ) { const point = coordSys.dataToRect([i], false).tl; // every rect const rect = new graphic.Rect({ shape: { x: point[0], y: point[1], width: sw, height: sh }, cursor: 'default', style: itemRectStyleModel }); group.add(rect); } } // render separate line _renderLines( calendarModel: CalendarModel, rangeData: CalendarParsedDateRangeInfo, orient: LayoutOrient, group: graphic.Group ) { const self = this; const coordSys = calendarModel.coordinateSystem; const lineStyleModel = calendarModel.getModel(['splitLine', 'lineStyle']).getLineStyle(); const show = calendarModel.get(['splitLine', 'show']); const lineWidth = lineStyleModel.lineWidth; this._tlpoints = []; this._blpoints = []; this._firstDayOfMonth = []; this._firstDayPoints = []; let firstDay = rangeData.start; for (let i = 0; firstDay.time <= rangeData.end.time; i++) { addPoints(firstDay.formatedDate); if (i === 0) { firstDay = coordSys.getDateInfo(rangeData.start.y + '-' + rangeData.start.m); } const date = firstDay.date; date.setMonth(date.getMonth() + 1); firstDay = coordSys.getDateInfo(date); } addPoints(coordSys.getNextNDay(rangeData.end.time, 1).formatedDate); function addPoints(date: OptionDataValueDate) { self._firstDayOfMonth.push(coordSys.getDateInfo(date)); self._firstDayPoints.push(coordSys.dataToRect([date], false).tl); const points = self._getLinePointsOfOneWeek(calendarModel, date, orient); self._tlpoints.push(points[0]); self._blpoints.push(points[points.length - 1]); show && self._drawSplitline(points, lineStyleModel, group); } // render top/left line show && this._drawSplitline(self._getEdgesPoints(self._tlpoints, lineWidth, orient), lineStyleModel, group); // render bottom/right line show && this._drawSplitline(self._getEdgesPoints(self._blpoints, lineWidth, orient), lineStyleModel, group); } // get points at both ends _getEdgesPoints(points: number[][], lineWidth: number, orient: LayoutOrient) { const rs = [points[0].slice(), points[points.length - 1].slice()]; const idx = orient === 'horizontal' ? 0 : 1; // both ends of the line are extend half lineWidth rs[0][idx] = rs[0][idx] - lineWidth / 2; rs[1][idx] = rs[1][idx] + lineWidth / 2; return rs; } // render split line _drawSplitline(points: number[][], lineStyle: PathStyleProps, group: graphic.Group) { const poyline = new graphic.Polyline({ z2: 20, shape: { points: points }, style: lineStyle }); group.add(poyline); } // render month line of one week points _getLinePointsOfOneWeek(calendarModel: CalendarModel, date: OptionDataValueDate, orient: LayoutOrient) { const coordSys = calendarModel.coordinateSystem; const parsedDate = coordSys.getDateInfo(date); const points = []; for (let i = 0; i < 7; i++) { const tmpD = coordSys.getNextNDay(parsedDate.time, i); const point = coordSys.dataToRect([tmpD.time], false); points[2 * tmpD.day] = point.tl; points[2 * tmpD.day + 1] = point[orient === 'horizontal' ? 'bl' : 'tr']; } return points; } _formatterLabel( formatter: string | ((params: T) => string), params: T ) { if (isString(formatter) && formatter) { return formatTplSimple(formatter, params); } if (isFunction(formatter)) { return formatter(params); } return params.nameMap; } _yearTextPositionControl( textEl: graphic.Text, point: number[], orient: LayoutOrient, position: 'left' | 'right' | 'top' | 'bottom', margin: number ): TextProps { let x = point[0]; let y = point[1]; let aligns: [ZRTextAlign, ZRTextVerticalAlign] = ['center', 'bottom']; if (position === 'bottom') { y += margin; aligns = ['center', 'top']; } else if (position === 'left') { x -= margin; } else if (position === 'right') { x += margin; aligns = ['center', 'top']; } else { // top y -= margin; } let rotate = 0; if (position === 'left' || position === 'right') { rotate = Math.PI / 2; } return { rotation: rotate, x, y, style: { align: aligns[0], verticalAlign: aligns[1] } }; } // render year _renderYearText( calendarModel: CalendarModel, rangeData: CalendarParsedDateRangeInfo, orient: LayoutOrient, group: graphic.Group ) { const yearLabel = calendarModel.getModel('yearLabel'); if (!yearLabel.get('show')) { return; } const margin = yearLabel.get('margin'); let pos = yearLabel.get('position'); if (!pos) { pos = orient !== 'horizontal' ? 'top' : 'left'; } const points = [this._tlpoints[this._tlpoints.length - 1], this._blpoints[0]]; const xc = (points[0][0] + points[1][0]) / 2; const yc = (points[0][1] + points[1][1]) / 2; const idx = orient === 'horizontal' ? 0 : 1; const posPoints = { top: [xc, points[idx][1]], bottom: [xc, points[1 - idx][1]], left: [points[1 - idx][0], yc], right: [points[idx][0], yc] }; let name = rangeData.start.y; if (+rangeData.end.y > +rangeData.start.y) { name = name + '-' + rangeData.end.y; } const formatter = yearLabel.get('formatter'); const params = { start: rangeData.start.y, end: rangeData.end.y, nameMap: name }; const content = this._formatterLabel(formatter, params); const yearText = new graphic.Text({ z2: 30, style: createTextStyle(yearLabel, { text: content }) }); yearText.attr(this._yearTextPositionControl(yearText, posPoints[pos], orient, pos, margin)); group.add(yearText); } _monthTextPositionControl( point: number[], isCenter: boolean, orient: LayoutOrient, position: 'start' | 'end', margin: number ): TextStyleProps { let align: ZRTextAlign = 'left'; let vAlign: ZRTextVerticalAlign = 'top'; let x = point[0]; let y = point[1]; if (orient === 'horizontal') { y = y + margin; if (isCenter) { align = 'center'; } if (position === 'start') { vAlign = 'bottom'; } } else { x = x + margin; if (isCenter) { vAlign = 'middle'; } if (position === 'start') { align = 'right'; } } return { x: x, y: y, align: align, verticalAlign: vAlign }; } // render month and year text _renderMonthText( calendarModel: CalendarModel, localeModel: Model, orient: LayoutOrient, group: graphic.Group ) { const monthLabel = calendarModel.getModel('monthLabel'); if (!monthLabel.get('show')) { return; } let nameMap = monthLabel.get('nameMap'); let margin = monthLabel.get('margin'); const pos = monthLabel.get('position'); const align = monthLabel.get('align'); const termPoints = [this._tlpoints, this._blpoints]; if (!nameMap || isString(nameMap)) { if (nameMap) { // case-sensitive localeModel = getLocaleModel(nameMap as string) || localeModel; } // PENDING // for ZH locale, original form is `δΈ€ζœˆ` but current form is `1月` nameMap = localeModel.get(['time', 'monthAbbr']) || []; } const idx = pos === 'start' ? 0 : 1; const axis = orient === 'horizontal' ? 0 : 1; margin = pos === 'start' ? -margin : margin; const isCenter = (align === 'center'); for (let i = 0; i < termPoints[idx].length - 1; i++) { const tmp = termPoints[idx][i].slice(); const firstDay = this._firstDayOfMonth[i]; if (isCenter) { const firstDayPoints = this._firstDayPoints[i]; tmp[axis] = (firstDayPoints[axis] + termPoints[0][i + 1][axis]) / 2; } const formatter = monthLabel.get('formatter'); const name = nameMap[+firstDay.m - 1]; const params = { yyyy: firstDay.y, yy: (firstDay.y + '').slice(2), MM: firstDay.m, M: +firstDay.m, nameMap: name }; const content = this._formatterLabel(formatter, params); const monthText = new graphic.Text({ z2: 30, style: extend( createTextStyle(monthLabel, {text: content}), this._monthTextPositionControl(tmp, isCenter, orient, pos, margin) ) }); group.add(monthText); } } _weekTextPositionControl( point: number[], orient: LayoutOrient, position: 'start' | 'end', margin: number, cellSize: number[] ): TextStyleProps { let align: ZRTextAlign = 'center'; let vAlign: ZRTextVerticalAlign = 'middle'; let x = point[0]; let y = point[1]; const isStart = position === 'start'; if (orient === 'horizontal') { x = x + margin + (isStart ? 1 : -1) * cellSize[0] / 2; align = isStart ? 'right' : 'left'; } else { y = y + margin + (isStart ? 1 : -1) * cellSize[1] / 2; vAlign = isStart ? 'bottom' : 'top'; } return { x: x, y: y, align: align, verticalAlign: vAlign }; } // render weeks _renderWeekText( calendarModel: CalendarModel, localeModel: Model, rangeData: CalendarParsedDateRangeInfo, orient: LayoutOrient, group: graphic.Group ) { const dayLabel = calendarModel.getModel('dayLabel'); if (!dayLabel.get('show')) { return; } const coordSys = calendarModel.coordinateSystem; const pos = dayLabel.get('position'); let nameMap = dayLabel.get('nameMap'); let margin = dayLabel.get('margin'); const firstDayOfWeek = coordSys.getFirstDayOfWeek(); if (!nameMap || isString(nameMap)) { if (nameMap) { // case-sensitive localeModel = getLocaleModel(nameMap as string) || localeModel; } // Use the first letter of `dayOfWeekAbbr` if `dayOfWeekShort` doesn't exist in the locale file const dayOfWeekShort = localeModel.get(['time', 'dayOfWeekShort' as any]); nameMap = dayOfWeekShort || map( localeModel.get(['time', 'dayOfWeekAbbr']), val => val[0] ); } let start = coordSys.getNextNDay( rangeData.end.time, (7 - rangeData.lweek) ).time; const cellSize = [coordSys.getCellWidth(), coordSys.getCellHeight()]; margin = parsePercent(margin, Math.min(cellSize[1], cellSize[0])); if (pos === 'start') { start = coordSys.getNextNDay( rangeData.start.time, -(7 + rangeData.fweek) ).time; margin = -margin; } for (let i = 0; i < 7; i++) { const tmpD = coordSys.getNextNDay(start, i); const point = coordSys.dataToRect([tmpD.time], false).center; let day = i; day = Math.abs((i + firstDayOfWeek) % 7); const weekText = new graphic.Text({ z2: 30, style: extend( createTextStyle(dayLabel, {text: nameMap[day]}), this._weekTextPositionControl(point, orient, pos, margin, cellSize) ) }); group.add(weekText); } } } export default CalendarView;