|  | /* | 
|  | * 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; |