import { AxisOptions, AxisLayoutData, CustomAxisOptions, AxisSourceData, AxisData, AxisLabelOptions, DataItem } from '../../models'; import { SeriesCategory } from '../../constants'; import { DefaultOptions, AxisName } from './config'; import { GlobalData } from '@constants/globalData'; import AtomComponent from '../../atomComponent'; import { computeAxisTickData, checkIsTickFromZero } from './utils'; import { datetime, merge } from '../../utils'; export default abstract class Axis extends AtomComponent { // 静态属性 axisName: string; axisIndex = 0; // y轴下标 options: AxisOptions & CustomAxisOptions = {}; seriesCategory = SeriesCategory.NONE; sourceData: AxisSourceData[]; layoutData: AxisLayoutData[] = []; constructor(chart, axisName: AxisName, axisdata: AxisData, options: AxisOptions, axisIndex: number) { super({ chart }); this.axisName = axisName; this.axisIndex = axisIndex; this.options = this.processOptions(options, DefaultOptions[axisName]); this.sourceData = this.preProcessData(axisdata); this.layoutData = this.processLayoutData(); this.processGlobalData(); } /** * 画坐标轴线 */ protected drawLine() {} /** * 画坐标轴刻度 */ protected drawTick() {} /** * 画坐标轴文本 */ protected drawLabel() {} /** * 画坐标轴分隔线 */ protected drawSplitLine() {} /** * 配置项解析 * 1.[合并]配置 2.[识别]排布方向 */ private processOptions(userOptions, defaultOptions) { const options = merge(defaultOptions, userOptions); const { type } = options; options.isVertical = (this.axisName === AxisName?.XAxis && type !== 'value') || (this.axisName === AxisName?.YAxis && type === 'value'); return options; } /** * 数据标准化 */ private preProcessData(axisdata: AxisData): AxisSourceData[] { const { type } = this.options; const isValueAxis = type === 'value'; // const isYAxis = this.axisName === AxisName.YAxis; let dataSrouce: DataItem[] = null; // valueAxis数据处理 if (isValueAxis) { dataSrouce = axisdata?.valueAxisDatas[this.axisIndex]; // if (isYAxis) return dataSrouce .filter(item => !item.isFake) .map(({ value }) => ({ value: `${value}`, isSampled: true, })); } // baseAxis数据处理 dataSrouce = axisdata?.baseAxisData; return dataSrouce // .filter(item => !item.isFake) .map(({ name }) => ({ value: name, isSampled: true, })); } /** * 数据采样 */ private sampleData(data: AxisSourceData[]) { const count = data.length; let { splitNumber = count } = this.options; if (count < splitNumber) splitNumber = count; const interval = Math.floor(count / splitNumber); return data.map((item, index) => { const { value } = item; return { value, isSampled: index % interval === 0, // 仅选取部分点作为样本点 }; }); } /** * 数据渲染布局 * @param data * @param axisLabel * @returns */ private makeLayoutData( isValueAxis: boolean, params?: { axisSourceDatas: AxisSourceData[]; axisLabel: AxisLabelOptions; }, ): AxisLayoutData[] { if (isValueAxis) { // value轴 const isTickFromZero = checkIsTickFromZero(this.series); const axisTickData = { axisSourceDatas: this.sourceData, isTickFromZero, }; const { standardTicks } = computeAxisTickData(axisTickData); return standardTicks.map((item: number) => ({ text: `${item}`, rotate: 0, isSampled: null, })); } // base轴 const { axisSourceDatas, axisLabel } = params; return axisSourceDatas.map((item: AxisSourceData) => { const { value, isSampled = false } = item; return ({ text: value, rotate: axisLabel.rotate, // || autoRotate, isSampled, }); }); } /* * 坐标轴布局、抽样 * 1.type=category -分类 数量多时 排布; (cate类型无数据抽样) * 2.type=time -针对timestamp类型处理 数据抽样算法扩展 * 3.type=value -针对横向排布图 * * 【ext】 * boundaryGap参数: * cate类型 true/false * value/time类型 [left, right]; */ private processLayoutData(): AxisLayoutData[] { if (!this.sourceData) return; const { type = 'category', axisLabel } = this.options; let layoutData: AxisLayoutData[] = []; switch (type) { case 'value': { // type='value':值的格式为number layoutData = this.makeLayoutData(true); break; } case 'category': { // type='category':值的格式为string // 抽样 this.sourceData = this.sampleData(this.sourceData); // 布局 const params = { axisSourceDatas: this.sourceData, axisLabel, }; layoutData = this.makeLayoutData(false, params); break; } case 'time': { // type='time': 值的格式为timeStamp 或 string形式的时间描述 // 抽样 this.sourceData = this.sampleData(this.sourceData); // 刻度计算 const regularData = this.sourceData .map((item: AxisSourceData) => { const { value, isSampled } = item; if (!value) return { value: '', isSampled, }; if (!datetime.isValid(value)) { console.warn(`时间字符串【${value}】格式异常`); return { value: '', isSampled, }; } const labelFormatter = axisLabel?.formatter as string; return { value: datetime.format(value, labelFormatter), isSampled, }; }); // 布局 const params = { axisSourceDatas: regularData, axisLabel, }; layoutData = this.makeLayoutData(false, params); break; } case 'log': default: break; } return layoutData; } /** * xAxis全局数据处理 */ private processGlobalData() { const { chart, canvas: { size }, options: { isVertical, type }, } = this; // 1.向组织&传递数据 const isNeedMarkLineData = this.axisName === AxisName.XAxis && isVertical; if (isNeedMarkLineData) { const tickGap = size.width / (this.options?.boundaryGap ? this.sourceData.length : this.sourceData.length - 1); const axisSourceData = this.sourceData.map((item, i) => { const { value, isSampled }: AxisSourceData = item; return { x: i * tickGap, value, isSampled, }; }); const axisGlobalData = { axisSourceData, axisType: this.options.type, }; chart.setGlobalData(GlobalData.BaseAxis4MarkLine, axisGlobalData); } // 2.向、... const isNeedBaseAxisData = type === 'value'; if (isNeedBaseAxisData) { const isTickFromZero = checkIsTickFromZero(this.series); const axisTickData = { axisSourceDatas: this.sourceData, isTickFromZero, }; const { tickMin, tickMax } = computeAxisTickData(axisTickData); const curValueAxisData = { axisIndex: this.axisIndex, axis: { max: tickMax, min: tickMin, }, data: this.sourceData.map(item => item.value || 0), }; const preValueAxisData = chart.getGlobalData(GlobalData.ValueAxis); const valueAxisData = !preValueAxisData ? [curValueAxisData] : [...preValueAxisData, curValueAxisData]; chart.setGlobalData(GlobalData.ValueAxis, valueAxisData); } } }