Spaces:
Running
Running
| /* | |
| * 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. | |
| */ | |
| /* global document */ | |
| import * as echarts from '../../../core/echarts'; | |
| import * as zrUtil from 'zrender/src/core/util'; | |
| import GlobalModel from '../../../model/Global'; | |
| import SeriesModel from '../../../model/Series'; | |
| import { ToolboxFeature, ToolboxFeatureOption } from '../featureManager'; | |
| import { ColorString, ECUnitOption, SeriesOption, Payload, Dictionary } from '../../../util/types'; | |
| import ExtensionAPI from '../../../core/ExtensionAPI'; | |
| import { addEventListener } from 'zrender/src/core/event'; | |
| import Axis from '../../../coord/Axis'; | |
| import Cartesian2D from '../../../coord/cartesian/Cartesian2D'; | |
| import { warn } from '../../../util/log'; | |
| /* global document */ | |
| const BLOCK_SPLITER = new Array(60).join('-'); | |
| const ITEM_SPLITER = '\t'; | |
| type DataItem = { | |
| name: string | |
| value: number[] | number | |
| }; | |
| type DataList = (DataItem | number | number[])[]; | |
| interface ChangeDataViewPayload extends Payload { | |
| newOption: { | |
| series: SeriesOption[] | |
| } | |
| } | |
| interface SeriesGroupMeta { | |
| axisDim: string | |
| axisIndex: number | |
| } | |
| interface SeriesGroup { | |
| series: SeriesModel[] | |
| categoryAxis: Axis | |
| valueAxis: Axis | |
| } | |
| /** | |
| * Group series into two types | |
| * 1. on category axis, like line, bar | |
| * 2. others, like scatter, pie | |
| */ | |
| function groupSeries(ecModel: GlobalModel) { | |
| const seriesGroupByCategoryAxis: Dictionary<SeriesGroup> = {}; | |
| const otherSeries: SeriesModel[] = []; | |
| const meta: SeriesGroupMeta[] = []; | |
| ecModel.eachRawSeries(function (seriesModel) { | |
| const coordSys = seriesModel.coordinateSystem; | |
| if (coordSys && (coordSys.type === 'cartesian2d' || coordSys.type === 'polar')) { | |
| // TODO: TYPE Consider polar? Include polar may increase unecessary bundle size. | |
| const baseAxis = (coordSys as Cartesian2D).getBaseAxis(); | |
| if (baseAxis.type === 'category') { | |
| const key = baseAxis.dim + '_' + baseAxis.index; | |
| if (!seriesGroupByCategoryAxis[key]) { | |
| seriesGroupByCategoryAxis[key] = { | |
| categoryAxis: baseAxis, | |
| valueAxis: coordSys.getOtherAxis(baseAxis), | |
| series: [] | |
| }; | |
| meta.push({ | |
| axisDim: baseAxis.dim, | |
| axisIndex: baseAxis.index | |
| }); | |
| } | |
| seriesGroupByCategoryAxis[key].series.push(seriesModel); | |
| } | |
| else { | |
| otherSeries.push(seriesModel); | |
| } | |
| } | |
| else { | |
| otherSeries.push(seriesModel); | |
| } | |
| }); | |
| return { | |
| seriesGroupByCategoryAxis: seriesGroupByCategoryAxis, | |
| other: otherSeries, | |
| meta: meta | |
| }; | |
| } | |
| /** | |
| * Assemble content of series on cateogory axis | |
| * @inner | |
| */ | |
| function assembleSeriesWithCategoryAxis(groups: Dictionary<SeriesGroup>): string { | |
| const tables: string[] = []; | |
| zrUtil.each(groups, function (group, key) { | |
| const categoryAxis = group.categoryAxis; | |
| const valueAxis = group.valueAxis; | |
| const valueAxisDim = valueAxis.dim; | |
| const headers = [' '].concat(zrUtil.map(group.series, function (series) { | |
| return series.name; | |
| })); | |
| // @ts-ignore TODO Polar | |
| const columns = [categoryAxis.model.getCategories()]; | |
| zrUtil.each(group.series, function (series) { | |
| const rawData = series.getRawData(); | |
| columns.push(series.getRawData().mapArray(rawData.mapDimension(valueAxisDim), function (val) { | |
| return val; | |
| })); | |
| }); | |
| // Assemble table content | |
| const lines = [headers.join(ITEM_SPLITER)]; | |
| for (let i = 0; i < columns[0].length; i++) { | |
| const items = []; | |
| for (let j = 0; j < columns.length; j++) { | |
| items.push(columns[j][i]); | |
| } | |
| lines.push(items.join(ITEM_SPLITER)); | |
| } | |
| tables.push(lines.join('\n')); | |
| }); | |
| return tables.join('\n\n' + BLOCK_SPLITER + '\n\n'); | |
| } | |
| /** | |
| * Assemble content of other series | |
| */ | |
| function assembleOtherSeries(series: SeriesModel[]) { | |
| return zrUtil.map(series, function (series) { | |
| const data = series.getRawData(); | |
| const lines = [series.name]; | |
| const vals: string[] = []; | |
| data.each(data.dimensions, function () { | |
| const argLen = arguments.length; | |
| const dataIndex = arguments[argLen - 1]; | |
| const name = data.getName(dataIndex); | |
| for (let i = 0; i < argLen - 1; i++) { | |
| vals[i] = arguments[i]; | |
| } | |
| lines.push((name ? (name + ITEM_SPLITER) : '') + vals.join(ITEM_SPLITER)); | |
| }); | |
| return lines.join('\n'); | |
| }).join('\n\n' + BLOCK_SPLITER + '\n\n'); | |
| } | |
| function getContentFromModel(ecModel: GlobalModel) { | |
| const result = groupSeries(ecModel); | |
| return { | |
| value: zrUtil.filter([ | |
| assembleSeriesWithCategoryAxis(result.seriesGroupByCategoryAxis), | |
| assembleOtherSeries(result.other) | |
| ], function (str) { | |
| return !!str.replace(/[\n\t\s]/g, ''); | |
| }).join('\n\n' + BLOCK_SPLITER + '\n\n'), | |
| meta: result.meta | |
| }; | |
| } | |
| function trim(str: string) { | |
| return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); | |
| } | |
| /** | |
| * If a block is tsv format | |
| */ | |
| function isTSVFormat(block: string): boolean { | |
| // Simple method to find out if a block is tsv format | |
| const firstLine = block.slice(0, block.indexOf('\n')); | |
| if (firstLine.indexOf(ITEM_SPLITER) >= 0) { | |
| return true; | |
| } | |
| } | |
| const itemSplitRegex = new RegExp('[' + ITEM_SPLITER + ']+', 'g'); | |
| /** | |
| * @param {string} tsv | |
| * @return {Object} | |
| */ | |
| function parseTSVContents(tsv: string) { | |
| const tsvLines = tsv.split(/\n+/g); | |
| const headers = trim(tsvLines.shift()).split(itemSplitRegex); | |
| const categories: string[] = []; | |
| const series: {name: string, data: string[]}[] = zrUtil.map(headers, function (header) { | |
| return { | |
| name: header, | |
| data: [] | |
| }; | |
| }); | |
| for (let i = 0; i < tsvLines.length; i++) { | |
| const items = trim(tsvLines[i]).split(itemSplitRegex); | |
| categories.push(items.shift()); | |
| for (let j = 0; j < items.length; j++) { | |
| series[j] && (series[j].data[i] = items[j]); | |
| } | |
| } | |
| return { | |
| series: series, | |
| categories: categories | |
| }; | |
| } | |
| function parseListContents(str: string) { | |
| const lines = str.split(/\n+/g); | |
| const seriesName = trim(lines.shift()); | |
| const data: DataList = []; | |
| for (let i = 0; i < lines.length; i++) { | |
| // if line is empty, ignore it. | |
| // there is a case that a user forgot to delete `\n`. | |
| const line = trim(lines[i]); | |
| if (!line) { | |
| continue; | |
| } | |
| let items = line.split(itemSplitRegex); | |
| let name = ''; | |
| let value: number[]; | |
| let hasName = false; | |
| if (isNaN(items[0] as unknown as number)) { // First item is name | |
| hasName = true; | |
| name = items[0]; | |
| items = items.slice(1); | |
| data[i] = { | |
| name: name, | |
| value: [] | |
| }; | |
| value = (data[i] as DataItem).value as number[]; | |
| } | |
| else { | |
| value = data[i] = []; | |
| } | |
| for (let j = 0; j < items.length; j++) { | |
| value.push(+items[j]); | |
| } | |
| if (value.length === 1) { | |
| hasName ? ((data[i] as DataItem).value = value[0]) : (data[i] = value[0]); | |
| } | |
| } | |
| return { | |
| name: seriesName, | |
| data: data | |
| }; | |
| } | |
| function parseContents(str: string, blockMetaList: SeriesGroupMeta[]) { | |
| const blocks = str.split(new RegExp('\n*' + BLOCK_SPLITER + '\n*', 'g')); | |
| const newOption: ECUnitOption = { | |
| series: [] | |
| }; | |
| zrUtil.each(blocks, function (block, idx) { | |
| if (isTSVFormat(block)) { | |
| const result = parseTSVContents(block); | |
| const blockMeta = blockMetaList[idx]; | |
| const axisKey = blockMeta.axisDim + 'Axis'; | |
| if (blockMeta) { | |
| newOption[axisKey] = newOption[axisKey] || []; | |
| (newOption[axisKey] as any)[blockMeta.axisIndex] = { | |
| data: result.categories | |
| }; | |
| newOption.series = (newOption.series as SeriesOption[]).concat(result.series); | |
| } | |
| } | |
| else { | |
| const result = parseListContents(block); | |
| (newOption.series as SeriesOption[]).push(result); | |
| } | |
| }); | |
| return newOption; | |
| } | |
| export interface ToolboxDataViewFeatureOption extends ToolboxFeatureOption { | |
| readOnly?: boolean | |
| optionToContent?: (option: ECUnitOption) => string | HTMLElement | |
| contentToOption?: (viewMain: HTMLDivElement, oldOption: ECUnitOption) => ECUnitOption | |
| icon?: string | |
| title?: string | |
| lang?: string[] | |
| backgroundColor?: ColorString | |
| textColor?: ColorString | |
| textareaColor?: ColorString | |
| textareaBorderColor?: ColorString | |
| buttonColor?: ColorString | |
| buttonTextColor?: ColorString | |
| } | |
| class DataView extends ToolboxFeature<ToolboxDataViewFeatureOption> { | |
| private _dom: HTMLDivElement; | |
| onclick(ecModel: GlobalModel, api: ExtensionAPI) { | |
| // FIXME: better way? | |
| setTimeout(() => { | |
| api.dispatchAction({ | |
| type: 'hideTip' | |
| }); | |
| }); | |
| const container = api.getDom(); | |
| const model = this.model; | |
| if (this._dom) { | |
| container.removeChild(this._dom); | |
| } | |
| const root = document.createElement('div'); | |
| // use padding to avoid 5px whitespace | |
| root.style.cssText = 'position:absolute;top:0;bottom:0;left:0;right:0;padding:5px'; | |
| root.style.backgroundColor = model.get('backgroundColor') || '#fff'; | |
| // Create elements | |
| const header = document.createElement('h4'); | |
| const lang = model.get('lang') || []; | |
| header.innerHTML = lang[0] || model.get('title'); | |
| header.style.cssText = 'margin:10px 20px'; | |
| header.style.color = model.get('textColor'); | |
| const viewMain = document.createElement('div'); | |
| const textarea = document.createElement('textarea'); | |
| viewMain.style.cssText = 'overflow:auto'; | |
| const optionToContent = model.get('optionToContent'); | |
| const contentToOption = model.get('contentToOption'); | |
| const result = getContentFromModel(ecModel); | |
| if (zrUtil.isFunction(optionToContent)) { | |
| const htmlOrDom = optionToContent(api.getOption()); | |
| if (zrUtil.isString(htmlOrDom)) { | |
| viewMain.innerHTML = htmlOrDom; | |
| } | |
| else if (zrUtil.isDom(htmlOrDom)) { | |
| viewMain.appendChild(htmlOrDom); | |
| } | |
| } | |
| else { | |
| // Use default textarea | |
| textarea.readOnly = model.get('readOnly'); | |
| const style = textarea.style; | |
| // eslint-disable-next-line max-len | |
| style.cssText = 'display:block;width:100%;height:100%;font-family:monospace;font-size:14px;line-height:1.6rem;resize:none;box-sizing:border-box;outline:none'; | |
| style.color = model.get('textColor'); | |
| style.borderColor = model.get('textareaBorderColor'); | |
| style.backgroundColor = model.get('textareaColor'); | |
| textarea.value = result.value; | |
| viewMain.appendChild(textarea); | |
| } | |
| const blockMetaList = result.meta; | |
| const buttonContainer = document.createElement('div'); | |
| buttonContainer.style.cssText = 'position:absolute;bottom:5px;left:0;right:0'; | |
| // eslint-disable-next-line max-len | |
| let buttonStyle = 'float:right;margin-right:20px;border:none;cursor:pointer;padding:2px 5px;font-size:12px;border-radius:3px'; | |
| const closeButton = document.createElement('div'); | |
| const refreshButton = document.createElement('div'); | |
| buttonStyle += ';background-color:' + model.get('buttonColor'); | |
| buttonStyle += ';color:' + model.get('buttonTextColor'); | |
| const self = this; | |
| function close() { | |
| container.removeChild(root); | |
| self._dom = null; | |
| } | |
| addEventListener(closeButton, 'click', close); | |
| addEventListener(refreshButton, 'click', function () { | |
| if ((contentToOption == null && optionToContent != null) | |
| || (contentToOption != null && optionToContent == null)) { | |
| if (__DEV__) { | |
| // eslint-disable-next-line | |
| warn('It seems you have just provided one of `contentToOption` and `optionToContent` functions but missed the other one. Data change is ignored.') | |
| } | |
| close(); | |
| return; | |
| } | |
| let newOption; | |
| try { | |
| if (zrUtil.isFunction(contentToOption)) { | |
| newOption = contentToOption(viewMain, api.getOption()); | |
| } | |
| else { | |
| newOption = parseContents(textarea.value, blockMetaList); | |
| } | |
| } | |
| catch (e) { | |
| close(); | |
| throw new Error('Data view format error ' + e); | |
| } | |
| if (newOption) { | |
| api.dispatchAction({ | |
| type: 'changeDataView', | |
| newOption: newOption | |
| }); | |
| } | |
| close(); | |
| }); | |
| closeButton.innerHTML = lang[1]; | |
| refreshButton.innerHTML = lang[2]; | |
| refreshButton.style.cssText = | |
| closeButton.style.cssText = buttonStyle; | |
| !model.get('readOnly') && buttonContainer.appendChild(refreshButton); | |
| buttonContainer.appendChild(closeButton); | |
| root.appendChild(header); | |
| root.appendChild(viewMain); | |
| root.appendChild(buttonContainer); | |
| viewMain.style.height = (container.clientHeight - 80) + 'px'; | |
| container.appendChild(root); | |
| this._dom = root; | |
| } | |
| remove(ecModel: GlobalModel, api: ExtensionAPI) { | |
| this._dom && api.getDom().removeChild(this._dom); | |
| } | |
| dispose(ecModel: GlobalModel, api: ExtensionAPI) { | |
| this.remove(ecModel, api); | |
| } | |
| static getDefaultOption(ecModel: GlobalModel) { | |
| const defaultOption: ToolboxDataViewFeatureOption = { | |
| show: true, | |
| readOnly: false, | |
| optionToContent: null, | |
| contentToOption: null, | |
| // eslint-disable-next-line | |
| icon: 'M17.5,17.3H33 M17.5,17.3H33 M45.4,29.5h-28 M11.5,2v56H51V14.8L38.4,2H11.5z M38.4,2.2v12.7H51 M45.4,41.7h-28', | |
| title: ecModel.getLocaleModel().get(['toolbox', 'dataView', 'title']), | |
| lang: ecModel.getLocaleModel().get(['toolbox', 'dataView', 'lang']), | |
| backgroundColor: '#fff', | |
| textColor: '#000', | |
| textareaColor: '#fff', | |
| textareaBorderColor: '#333', | |
| buttonColor: '#c23531', | |
| buttonTextColor: '#fff' | |
| }; | |
| return defaultOption; | |
| } | |
| } | |
| /** | |
| * @inner | |
| */ | |
| function tryMergeDataOption(newData: DataList, originalData: DataList) { | |
| return zrUtil.map(newData, function (newVal, idx) { | |
| const original = originalData && originalData[idx]; | |
| if (zrUtil.isObject(original) && !zrUtil.isArray(original)) { | |
| const newValIsObject = zrUtil.isObject(newVal) && !zrUtil.isArray(newVal); | |
| if (!newValIsObject) { | |
| newVal = { | |
| value: newVal | |
| } as DataItem; | |
| } | |
| // original data has name but new data has no name | |
| const shouldDeleteName = original.name != null && (newVal as DataItem).name == null; | |
| // Original data has option | |
| newVal = zrUtil.defaults((newVal as DataItem), original); | |
| shouldDeleteName && (delete (newVal as DataItem).name); | |
| return newVal; | |
| } | |
| else { | |
| return newVal; | |
| } | |
| }); | |
| } | |
| // TODO: SELF REGISTERED. | |
| echarts.registerAction({ | |
| type: 'changeDataView', | |
| event: 'dataViewChanged', | |
| update: 'prepareAndUpdate' | |
| }, function (payload: ChangeDataViewPayload, ecModel: GlobalModel) { | |
| const newSeriesOptList: SeriesOption[] = []; | |
| zrUtil.each(payload.newOption.series, function (seriesOpt) { | |
| const seriesModel = ecModel.getSeriesByName(seriesOpt.name)[0]; | |
| if (!seriesModel) { | |
| // New created series | |
| // Geuss the series type | |
| newSeriesOptList.push(zrUtil.extend({ | |
| // Default is scatter | |
| type: 'scatter' | |
| }, seriesOpt)); | |
| } | |
| else { | |
| const originalData = seriesModel.get('data'); | |
| newSeriesOptList.push({ | |
| name: seriesOpt.name, | |
| data: tryMergeDataOption(seriesOpt.data as DataList, originalData as DataList) | |
| }); | |
| } | |
| }); | |
| ecModel.mergeOption(zrUtil.defaults({ | |
| series: newSeriesOptList | |
| }, payload.newOption)); | |
| }); | |
| export default DataView; | |