import { DEFAULT_SPLITER, DEFAULT_FONT_SIZE, DEFAULT_FONT_WEIGHT, DEFAULT_FONT_FAMILY, PIVOT_CELL_PADDING, PIVOT_CELL_BORDER, PIVOT_LINE_HEIGHT, PIVOT_MAX_CONTENT_WIDTH, PIVOT_CHART_ELEMENT_MIN_WIDTH, PIVOT_CHART_ELEMENT_MAX_WIDTH, PIVOT_CHART_METRIC_AXIS_MIN_SIZE, PIVOT_CHART_POINT_LIMIT, PIVOT_BORDER, PIVOT_XAXIS_SIZE, PIVOT_YAXIS_SIZE, PIVOT_TITLE_SIZE, PIVOT_XAXIS_ROTATE_LIMIT, PIVOT_XAXIS_TICK_SIZE, PIVOT_CANVAS_AXIS_SIZE_LIMIT, PIVOT_DEFAULT_SCATTER_SIZE_TIMES } from 'app/globalConstants' import { DimetionType, IChartStyles, IChartInfo } from './Widget' import { IChartLine, IChartUnit } from './Pivot/Chart' import { IDataParamSource } from './Workbench/Dropbox' import { getFieldAlias } from '../components/Config/Field' import { getFormattedValue } from '../components/Config/Format' import widgetlibs from '../config' import PivotTypes from '../config/pivot/PivotTypes' import ChartTypes from '../config/chart/ChartTypes' const pivotlibs = widgetlibs['pivot'] const chartlibs = widgetlibs['chart'] import { uuid } from 'utils/util' export function getAggregatorLocale(agg) { switch (agg) { case 'sum': return '总计' case 'avg': return '平均数' case 'count': return '计数' case 'COUNTDISTINCT': return '去重计数' case 'max': return '最大值' case 'min': return '最小值' case 'median': return '中位数' case 'percentile': return '百分位' case 'stddev': return '标准偏差' case 'var': return '方差' } } export function encodeMetricName(name) { return `${name}${DEFAULT_SPLITER}${uuid(8, 16)}` } export function decodeMetricName(encodedName) { return encodedName.split(DEFAULT_SPLITER)[0] } export function spanSize(arr, i, j) { if (i !== 0) { let noDraw = true for (let x = 0; x <= j; x += 1) { if (arr[i - 1][x] !== arr[i][x]) { noDraw = false } } if (noDraw) { return -1 } } let len = 0 while (i + len < arr.length) { let stop = false for (let x = 0; x <= j; x += 1) { if (arr[i][x] !== arr[i + len][x]) { stop = true } } if (stop) { break } len++ } return len } export function naturalSort(a, b): number { const rx = /(\d+)|(\D+)/g const rd = /\d/ const rz = /^0/ if (b != null && a == null) { return -1 } if (a != null && b == null) { return 1 } if (typeof a === 'number' && isNaN(a)) { return -1 } if (typeof b === 'number' && isNaN(b)) { return 1 } const na = +a const nb = +b if (na < nb) { return -1 } if (na > nb) { return 1 } if (typeof a === 'number' && typeof b !== 'number') { return -1 } if (typeof b === 'number' && typeof a !== 'number') { return 1 } if (typeof a === 'number' && typeof b === 'number') { return 0 } if (isNaN(nb) && !isNaN(na)) { return -1 } if (isNaN(na) && !isNaN(nb)) { return 1 } const sa = String(a) const sb = String(b) if (sa === sb) { return 0 } if (!(rd.test(sa) && rd.test(sb))) { return sa > sb ? 1 : -1 } const ra = sa.match(rx) const rb = sb.match(rx) while (ra.length && rb.length) { const a1 = ra.shift() const b1 = rb.shift() if (a1 !== b1) { if (rd.test(a1) && rd.test(b1)) { return Number(a1.replace(rz, '.0')) - Number(b1.replace(rz, '.0')) } else { return a1 > b1 ? 1 : -1 } } } return ra.length - rb.length } let utilCanvas = null export const getTextWidth = ( text: string, fontWeight: string = DEFAULT_FONT_WEIGHT, fontSize: string = DEFAULT_FONT_SIZE, fontFamily: string = DEFAULT_FONT_FAMILY ): number => { const canvas = utilCanvas || (utilCanvas = document.createElement('canvas')) const context = canvas.getContext('2d') context.font = `${fontWeight} ${fontSize} ${fontFamily}` const metrics = context.measureText(text) return Math.ceil(metrics.width) } export const getPivotContentTextWidth = ( text: string, fontWeight: string = DEFAULT_FONT_WEIGHT, fontSize: string = DEFAULT_FONT_SIZE, fontFamily: string = DEFAULT_FONT_FAMILY ): number => { return Math.min( getTextWidth(text, fontWeight, fontSize, fontFamily), PIVOT_MAX_CONTENT_WIDTH ) } export function getPivotCellWidth(width: number): number { return width + PIVOT_CELL_PADDING * 2 + PIVOT_CELL_BORDER * 2 } export function getPivotCellHeight(height?: number): number { return ( (height || PIVOT_LINE_HEIGHT) + PIVOT_CELL_PADDING * 2 + PIVOT_CELL_BORDER ) } export const getTableBodyWidth = ( direction: DimetionType, containerWidth, rowHeaderWidths ) => { const title = rowHeaderWidths.length && PIVOT_TITLE_SIZE const rowHeaderWidthSum = direction === 'row' ? rowHeaderWidths .slice(0, rowHeaderWidths.length - 1) .reduce((sum, r) => sum + getPivotCellWidth(r), 0) : rowHeaderWidths.reduce((sum, r) => sum + getPivotCellWidth(r), 0) return ( containerWidth - PIVOT_BORDER * 2 - rowHeaderWidthSum - PIVOT_YAXIS_SIZE - title ) } export const getTableBodyHeight = ( direction: DimetionType, containerHeight, columnHeaderCount ) => { const title = columnHeaderCount && PIVOT_TITLE_SIZE const realColumnHeaderCount = direction === 'col' ? Math.max(columnHeaderCount - 1, 0) : columnHeaderCount return ( containerHeight - PIVOT_BORDER * 2 - realColumnHeaderCount * getPivotCellHeight() - PIVOT_XAXIS_SIZE - title ) } export function getChartElementSize( direction: DimetionType, tableBodySideLength: number[], chartElementCountArr: number[], multiCoordinate: boolean ): number { let chartElementCount let side if (direction === 'col') { chartElementCount = Math.max(1, chartElementCountArr[0]) side = tableBodySideLength[0] } else { chartElementCount = Math.max(1, chartElementCountArr[1]) side = tableBodySideLength[1] } const sizePerElement = side / chartElementCount const limit = multiCoordinate ? PIVOT_CHART_METRIC_AXIS_MIN_SIZE : PIVOT_CHART_ELEMENT_MIN_WIDTH return Math.max(Math.floor(sizePerElement), limit) } export function shouldTableBodyCollapsed( direction: DimetionType, elementSize: number, tableBodyHeight: number, rowKeyLength: number ): boolean { return direction === 'row' && tableBodyHeight > rowKeyLength * elementSize } export function getChartUnitMetricWidth( tableBodyWidth, colKeyCount: number, metricCount: number ): number { const realContainerWidth = Math.max( tableBodyWidth, colKeyCount * metricCount * PIVOT_CHART_METRIC_AXIS_MIN_SIZE ) return realContainerWidth / colKeyCount / metricCount } export function getChartUnitMetricHeight( tableBodyHeight, rowKeyCount: number, metricCount: number ): number { const realContainerHeight = Math.max( tableBodyHeight, rowKeyCount * metricCount * PIVOT_CHART_METRIC_AXIS_MIN_SIZE ) return realContainerHeight / rowKeyCount / metricCount } export function checkChartEnable( dimensionCount: number, metricCount: number, charts: IChartInfo | IChartInfo[] ): boolean { const chartArr = Array.isArray(charts) ? charts : [charts] const enabled = chartArr.every(({ rules }) => { const currentRulesChecked = rules.some(({ dimension, metric }) => { if (Array.isArray(dimension)) { if (dimensionCount < dimension[0] || dimensionCount > dimension[1]) { return false } } else if (dimensionCount !== dimension) { return false } if (Array.isArray(metric)) { if (metricCount < metric[0] || metricCount > metric[1]) { return false } } else if (metricCount !== metric) { return false } return true }) return currentRulesChecked }) return enabled } export function getAxisInterval(max, splitNumber) { const roughInterval = Math.floor(max / splitNumber) const divisor = Math.pow(10, `${roughInterval}`.length - 1) return (Math.floor(roughInterval / divisor) + 1) * divisor } export function getChartPieces(total, lines) { if (lines === 1) { return lines } const eachLine = total / lines const pct = Math.abs(eachLine - PIVOT_CHART_POINT_LIMIT) / PIVOT_CHART_POINT_LIMIT return pct < 0.2 ? lines : eachLine > PIVOT_CHART_POINT_LIMIT ? lines : getChartPieces(total, Math.round(lines / 2)) } export function metricAxisLabelFormatter(value) { const positive = value >= 0 if (!positive) { value = -value } let endValue const orderLessKilo = (value) => { if (value < Math.pow(10, 3)) { endValue = value } } const orderKilo = (value) => { if (value >= Math.pow(10, 3) && value < Math.pow(10, 6)) { endValue = `${precision(value / Math.pow(10, 3))}K` } } const orderMillion = (value) => { if (value >= Math.pow(10, 6) && value < Math.pow(10, 9)) { endValue = `${precision(value / Math.pow(10, 6))}M` } } const orderBillion = (value) => { if (value >= Math.pow(10, 9) && value < Math.pow(10, 12)) { endValue = `${precision(value / Math.pow(10, 9))}B` } } const orderTrillion = (value) => { if (value >= Math.pow(10, 12)) { endValue = `${precision(value / Math.pow(10, 12))}T` } } const orderFn = (...fns) => (value) => fns.reduce((pre, fn) => fn.call(this, value), 0) orderFn( orderLessKilo, orderKilo, orderMillion, orderBillion, orderTrillion )(value) return positive ? endValue : `-${endValue}` function precision(num) { return num >= 10 ? Math.floor(num) : num.toFixed(1) } } export function getPivot(): IChartInfo { return pivotlibs.find((p) => p.id === PivotTypes.PivotTable) } export function getTable(): IChartInfo { return chartlibs.find((c) => c.id === ChartTypes.Table) } export function getPivotModeSelectedCharts( items: IDataParamSource[] ): IChartInfo[] { return items.length ? items.map((i) => i.chart) : [getPivot()] } export function getStyleConfig(chartStyles: IChartStyles): IChartStyles { return { ...chartStyles, pivot: chartStyles.pivot || { ...getPivot().style['pivot'] } // FIXME 兼容0.3.0-beta 数据库 } } export function getAxisData( type: 'x' | 'y', rowKeys, colKeys, rowTree, colTree, tree, metrics, drawingData, dimetionAxis ) { const { elementSize, unitMetricWidth, unitMetricHeight } = drawingData const data: IChartLine[] = [] const chartLine: IChartUnit[] = [] let axisLength = 0 let renderKeys let renderTree let sndKeys let sndTree let renderDimetionAxis let unitMetricSide if (type === 'x') { renderKeys = colKeys renderTree = colTree sndKeys = rowKeys sndTree = rowTree renderDimetionAxis = 'col' unitMetricSide = unitMetricWidth } else { renderKeys = rowKeys renderTree = rowTree sndKeys = colKeys sndTree = colTree renderDimetionAxis = 'row' unitMetricSide = unitMetricHeight } if (renderKeys.length) { renderKeys.forEach((keys, i) => { const flatKey = keys.join(String.fromCharCode(0)) const { records } = renderTree[flatKey] if (dimetionAxis === renderDimetionAxis) { const nextKeys = renderKeys[i + 1] || [] let lastUnit = chartLine[chartLine.length - 1] if (!lastUnit || lastUnit.ended) { lastUnit = { width: 0, records: [], ended: false } chartLine.push(lastUnit) } lastUnit.records.push({ key: keys[keys.length - 1], value: records[0] }) if ( (keys.length === 1 && i === renderKeys.length - 1) || keys[keys.length - 2] !== nextKeys[nextKeys.length - 2] ) { const unitLength = lastUnit.records.length * elementSize axisLength += unitLength lastUnit.width = unitLength lastUnit.ended = true } if (!nextKeys.length) { data.push({ key: flatKey, data: chartLine.slice() }) } } else { axisLength += unitMetricSide chartLine.push({ width: unitMetricSide, records: [ { key: keys[keys.length - 1], value: records[0] } ], ended: true }) if (i === renderKeys.length - 1) { data.push({ key: flatKey, data: chartLine.slice() }) } } }) } else { if (dimetionAxis !== renderDimetionAxis) { data.push({ key: uuid(8, 16), data: [ { width: unitMetricSide, records: [ { key: '', value: sndKeys.length ? Object.values(sndTree)[0] : tree[0] ? tree[0][0] : [] } ], ended: true } ] }) axisLength = unitMetricSide } } axisLength = dimetionAxis === renderDimetionAxis ? axisLength : axisLength * metrics.length return { data: axisDataCutting(type, dimetionAxis, metrics, axisLength, data), length: axisLength } } export function axisDataCutting( type: 'x' | 'y', dimetionAxis, metrics, axisLength, data ) { if (axisLength > PIVOT_CANVAS_AXIS_SIZE_LIMIT) { const result = [] data.forEach((line) => { let blockLine = { key: `${uuid(8, 16)}${line.key}`, data: [] } let block = { key: '', length: 0, data: [blockLine] } line.data.forEach((unit, index) => { const unitWidth = (type === 'x' && dimetionAxis === 'row') || (type === 'y' && dimetionAxis === 'col') ? unit.width * metrics.length : unit.width if (block.length + unitWidth > PIVOT_CANVAS_AXIS_SIZE_LIMIT) { block.key = `${index}${block.data.map((d) => d.key).join(',')}` result.push(block) blockLine = { key: `${uuid(8, 16)}${line.key}`, data: [] } block = { key: '', length: 0, data: [blockLine] } } block.length += unitWidth blockLine.data.push(unit) if (index === line.data.length - 1) { block.key = `${index}${block.data.map((d) => d.key).join(',')}` result.push(block) } }) }) return result } else { return [ { key: 'block', data, length: axisLength } ] } } export function getXaxisLabel(elementSize) { return function (label) { const originLabel = label const ellipsis = '…' const limit = elementSize > PIVOT_XAXIS_ROTATE_LIMIT ? elementSize : PIVOT_XAXIS_SIZE - PIVOT_XAXIS_TICK_SIZE while (getTextWidth(label) > limit) { label = label.substring(0, label.length - 1) } return label === originLabel ? label : `${label.substring(0, label.length - 1)}${ellipsis}` } } export function getTooltipPosition(point, params, dom, rect, size) { const [x, y] = point const { contentSize, viewSize } = size const [cx, cy] = contentSize const [vx, vy] = viewSize const distanceXToMouse = 10 return [ x + cx + distanceXToMouse > vx ? x - distanceXToMouse - cx : x + distanceXToMouse, // Math.min(x, vx - cx), Math.min(y, vy - cy) ] } export function getPivotTooltipLabel( seriesData, cols, rows, metrics, color, label, size, scatterXAxis, tip ) { let dimetionColumns = cols.concat(rows) let metricColumns = [...metrics] if (color) { dimetionColumns = dimetionColumns.concat(color.items.map((i) => i.name)) } if (label) { dimetionColumns = dimetionColumns.concat( label.items.filter((i) => i.type === 'category').map((i) => i.name) ) metricColumns = metricColumns.concat( label.items.filter((i) => i.type === 'value') ) } if (size) { metricColumns = metricColumns.concat(size.items) } if (scatterXAxis) { metricColumns = metricColumns.concat(scatterXAxis.items) } if (tip) { metricColumns = metricColumns.concat(tip.items) } dimetionColumns = dimetionColumns.reduce((arr, dc) => { if (!arr.includes(dc)) { arr.push(dc) } return arr }, []) metricColumns = metricColumns.reduce((arr, mc) => { const decodedName = decodeMetricName(mc.name) if ( !arr.find( (m) => decodeMetricName(m.name) === decodedName && m.agg === mc.agg ) ) { arr.push(mc) } return arr }, []) return function (params) { const record = getTriggeringRecord(params, seriesData) return metricColumns .map((mc) => { const decodedName = decodeMetricName(mc.name) const value = record ? Array.isArray(record) ? record.reduce((sum, r) => sum + r[`${mc.agg}(${decodedName})`], 0) : record[`${mc.agg}(${decodedName})`] : 0 return `${decodedName}: ${getFormattedValue(value, mc.format)}` }) .concat( dimetionColumns.map((dc) => { const value = record ? Array.isArray(record) ? record[0][dc] : record[dc] : '' return `${dc}: ${getFormattedValue(value, dc.format)}` }) ) .join('
') } } export function getChartTooltipLabel(type, seriesData, options) { const { cols, metrics, color, size, scatterXAxis, tip } = options let dimentionColumns: any[] = cols let metricColumns = [...metrics] if (color) { dimentionColumns = dimentionColumns.concat(color.items) } if (size) { metricColumns = metricColumns.concat(size.items) } if (scatterXAxis) { metricColumns = metricColumns.concat(scatterXAxis.items) } if (tip) { metricColumns = metricColumns.concat(tip.items) } dimentionColumns = dimentionColumns.filter( (dc, idx) => dimentionColumns.findIndex((c) => c.name === dc.name) === idx ) metricColumns = metricColumns.reduce((arr, mc) => { const decodedName = decodeMetricName(mc.name) if ( !arr.find( (m) => decodeMetricName(m.name) === decodedName && m.agg === mc.agg ) ) { arr.push(mc) } return arr }, []) return function (params) { const { componentType } = params if (componentType === 'markLine') { const { name, value } = params return `参考线
${name}: ${value}` } else if (componentType === 'markArea') { const { name, data: { coord } } = params const valueIndex = coord[0].findIndex( (c) => c !== Infinity && c !== -Infinity ) return `参考区间
${name}: ${coord[0][valueIndex]} - ${coord[1][valueIndex]}` } else { const { seriesIndex, dataIndex, color } = params const record = type === 'funnel' || type === 'map' ? seriesData[dataIndex] : seriesData[seriesIndex][dataIndex] let tooltipLabels = [] tooltipLabels = tooltipLabels.concat( dimentionColumns.map((dc) => { let value = record ? Array.isArray(record) ? record[0][dc.name] : record[dc.name] : '' value = getFormattedValue(value, dc.format) return `${getFieldAlias(dc.field, {}) || dc.name}: ${value}` // @FIXME dynamic field alias by queryVariable in dashboard }) ) tooltipLabels = tooltipLabels.concat( metricColumns.map((mc) => { const decodedName = decodeMetricName(mc.name) let value = record ? Array.isArray(record) ? record.reduce( (sum, r) => sum + r[`${mc.agg}(${decodedName})`], 0 ) : record[`${mc.agg}(${decodedName})`] : 0 value = getFormattedValue(value, mc.format) return `${getFieldAlias(mc.field, {}) || decodedName}: ${value}` }) ) if (color) { const circle = `` if (!dimentionColumns.length) { tooltipLabels.unshift(circle) } else { tooltipLabels[0] = circle + tooltipLabels[0] } } return tooltipLabels.join('
') } } } export function getChartLabel(seriesData, labelItem) { return function (params) { const record = getTriggeringRecord(params, seriesData) || {} return labelItem.type === 'category' ? Array.isArray(record) ? record[0][labelItem.name] : record[labelItem.name] || '' : Array.isArray(record) ? record.reduce( (sum, r) => sum + r[`${labelItem.agg}(${decodeMetricName(labelItem.name)})`], 0 ) : record[`${labelItem.agg}(${decodeMetricName(labelItem.name)})`] || 0 } } export function getTriggeringRecord(params, seriesData) { const { seriesIndex, dataIndex } = params const { type, grouped, records } = seriesData[seriesIndex] let record if (type === 'cartesian') { record = grouped ? records[dataIndex] : records[dataIndex].value } else if (type === 'polar') { record = records[dataIndex] } else { record = records ? records[0] : {} } return record } export function getSizeRate(min, max) { return Math.max(min / 10, max / 100) } export function getSizeValue(value) { return value >= PIVOT_DEFAULT_SCATTER_SIZE_TIMES ? value - PIVOT_DEFAULT_SCATTER_SIZE_TIMES + 1 : 1 / Math.pow(2, PIVOT_DEFAULT_SCATTER_SIZE_TIMES - value) } export const iconMapping = { line: 'icon-chart-line', bar: 'icon-chart-bar', scatter: 'icon-scatter-chart', pie: 'icon-chartpie', area: 'icon-area-chart', sankey: 'icon-kongjiansangjitu', funnel: 'icon-iconloudoutu', treemap: 'icon-chart-treemap', wordCloud: 'icon-chartwordcloud', table: 'icon-table', scorecard: 'icon-calendar1', text: 'icon-text', map: 'icon-china', doubleYAxis: 'icon-duplex', boxplot: 'icon-508tongji_xiangxiantu', markBoxplot: 'icon-508tongji_xiangxiantu', graph: 'icon-510tongji_guanxitu', waterfall: 'icon-waterfall', gauge: 'icon-gauge', radar: 'icon-radarchart', parallel: 'icon-parallel', confidenceBand: 'icon-confidence-band' } export function getCorrectInputNumber(num: any): number { switch (typeof num) { case 'string': if (!num.trim()) { return null } else { return Number(num) || null } return case 'number': return num default: return null } }