diff --git a/src/chart/funnel/FunnelSeries.ts b/src/chart/funnel/FunnelSeries.ts index 152cdb37da..83aa8112ce 100644 --- a/src/chart/funnel/FunnelSeries.ts +++ b/src/chart/funnel/FunnelSeries.ts @@ -48,6 +48,10 @@ type FunnelLabelOption = Omit & { | 'outer' | 'inner' | 'center' | 'rightTop' | 'rightBottom' | 'leftTop' | 'leftBottom' }; +type FunnelRateLabelOption = Omit & { + precision: number +}; + interface FunnelStatesMixin { emphasis?: DefaultStatesMixinEmphasis } @@ -59,6 +63,8 @@ export interface FunnelStateOption { itemStyle?: ItemStyleOption label?: FunnelLabelOption labelLine?: LabelLineOption + rateLabel?: FunnelRateLabelOption + overallRateLabel?: FunnelRateLabelOption } export interface FunnelDataItemOption @@ -95,6 +101,12 @@ export interface FunnelSeriesOption funnelAlign?: HorizontalAlign | VerticalAlign data?: (OptionDataValueNumeric | OptionDataValueNumeric[] | FunnelDataItemOption)[] + + exitWidth?: string + + showRate?: boolean + + dynamicHeight?: boolean } class FunnelSeriesModel extends SeriesModel { @@ -172,6 +184,14 @@ class FunnelSeriesModel extends SeriesModel { position: 'outer' // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调 }, + rateLabel: { + show: true, + precision: 2 + }, + overallRateLabel: { + show: true, + precision: 2 + }, labelLine: { show: true, length: 20, diff --git a/src/chart/funnel/FunnelView.ts b/src/chart/funnel/FunnelView.ts index e4a922afca..60b49a7076 100644 --- a/src/chart/funnel/FunnelView.ts +++ b/src/chart/funnel/FunnelView.ts @@ -16,27 +16,100 @@ * specific language governing permissions and limitations * under the License. */ - +import * as zrUtil from 'zrender/src/core/util'; import * as graphic from '../../util/graphic'; import { setStatesStylesFromModel, toggleHoverEmphasis } from '../../util/states'; import ChartView from '../../view/Chart'; -import FunnelSeriesModel, {FunnelDataItemOption} from './FunnelSeries'; +import FunnelSeriesModel, { FunnelDataItemOption } from './FunnelSeries'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import SeriesData from '../../data/SeriesData'; -import { ColorString } from '../../util/types'; +import { + ColorString, + DisplayState, + InterpolatableValue, + SeriesDataType +} from '../../util/types'; import { setLabelLineStyle, getLabelLineStatesModels } from '../../label/labelGuideHelper'; import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; import { saveOldStyle } from '../../animation/basicTransition'; const opacityAccessPath = ['itemStyle', 'opacity'] as const; +const rateLabelFetcher = { + getFormattedLabel( + // In MapDraw case it can be string (region name) + labelDataIndex: number, + status: DisplayState, + dataType?: SeriesDataType, + labelDimIndex?: number, + formatter?: string | ((params: object) => string), + // If provided, the implementation of `getFormattedLabel` can use it + // to generate the final label text. + extendParams?: { + interpolatedValue: InterpolatableValue + } + ): string { + status = status || 'normal'; + const { hostModel, layout } = this as unknown as { hostModel: FunnelSeriesModel, layout: any }; + const data = hostModel.getData(dataType); + + if (!formatter) { + const itemModel = data.getItemModel(labelDataIndex); + // @ts-ignore + formatter = itemModel.get(status === 'normal' + ? ['rateLabel', 'formatter'] + : [status, 'rateLabel', 'formatter'] + ); + } + + const { rate, isLastPiece, nextName, preName, preDataIndex, nextDataIndex } = layout; + + if (isLastPiece) { + const itemModel = data.getItemModel(labelDataIndex); + // @ts-ignore + formatter = itemModel.get(status === 'normal' + ? ['overallRateLabel', 'formatter'] + : [status, 'overallRateLabel', 'formatter'] + ); + } + + type RateParams = { + rate: string, + preName: string, + nextName: string, + preDataIndex: string, + nextDataIndex: string, + formatter: string | ((params: object) => string) + }; + + const params: RateParams = { + rate, // a + preName, // b + nextName, // c + preDataIndex, // d + nextDataIndex, // e + formatter + }; + + if (zrUtil.isFunction(formatter)) { + return formatter(params); + } + + return ''; + } +}; + /** * Piece of pie including Sector, Label, LabelLine */ class FunnelPiece extends graphic.Polygon { - constructor(data: SeriesData, idx: number) { + /** + * @param type judge is data blocks or conversion blocks + */ + + constructor(data: SeriesData, idx: number, type: 'data' | 'rate') { super(); const polygon = this; @@ -45,10 +118,10 @@ class FunnelPiece extends graphic.Polygon { polygon.setTextContent(text); this.setTextGuideLine(labelLine); - this.updateData(data, idx, true); + this.updateData(data, idx, type, true); } - updateData(data: SeriesData, idx: number, firstCreate?: boolean) { + updateData(data: SeriesData, idx: number, type: 'data' | 'rate', firstCreate?: boolean) { const polygon = this; @@ -58,17 +131,29 @@ class FunnelPiece extends graphic.Polygon { const emphasisModel = itemModel.getModel('emphasis'); let opacity = itemModel.get(opacityAccessPath); opacity = opacity == null ? 1 : opacity; + if (type === 'rate') { + // the opacity of rate piece is half of data + opacity /= 2; + } if (!firstCreate) { saveOldStyle(polygon); } + // hide the last rate piece and when it not the last one show it again + polygon.invisible = false; + if (layout.isLastPiece && type === 'rate') { + // hide last rate piece + polygon.invisible = true; + } // Update common style polygon.useStyle(data.getItemVisual(idx, 'style')); polygon.style.lineJoin = 'round'; + const points = type === 'data' ? layout.points : layout.ratePoints; + if (firstCreate) { polygon.setShape({ - points: layout.points + points }); polygon.style.opacity = 0; graphic.initProps(polygon, { @@ -83,49 +168,62 @@ class FunnelPiece extends graphic.Polygon { opacity: opacity }, shape: { - points: layout.points + points } }, seriesModel, idx); } - setStatesStylesFromModel(polygon, itemModel); - - this._updateLabel(data, idx); + this._updateLabel(data, idx, type); - toggleHoverEmphasis( - this, - emphasisModel.get('focus'), - emphasisModel.get('blurScope'), - emphasisModel.get('disabled') - ); + setStatesStylesFromModel(polygon, itemModel); + if (type === 'data') { + toggleHoverEmphasis( + this, + emphasisModel.get('focus'), + emphasisModel.get('blurScope'), + emphasisModel.get('disabled') + ); + } } - _updateLabel(data: SeriesData, idx: number) { + _updateLabel(data: SeriesData, idx: number, type: 'data' | 'rate') { const polygon = this; const labelLine = this.getTextGuideLine(); const labelText = polygon.getTextContent(); - const seriesModel = data.hostModel; + const seriesModel = data.hostModel as FunnelSeriesModel; const itemModel = data.getItemModel(idx); const layout = data.getItemLayout(idx); - const labelLayout = layout.label; + const labelLayout = layout[type === 'data' ? 'label' : 'rateLabel']; const style = data.getItemVisual(idx, 'style'); const visualColor = style.fill as ColorString; + // bind this to data of rateLabelFetherFunc + let rateFetcher: any; // clone default fechter + if (type === 'rate') { + rateFetcher = { + getFormattedLabel: rateLabelFetcher.getFormattedLabel.bind( + { hostModel: data.hostModel, layout } + ) + }; + } + const rateLabel = layout.isLastPiece ? 'overallRateLabel' : 'rateLabel'; setLabelStyle( // position will not be used in setLabelStyle labelText, - getLabelStatesModels(itemModel), + getLabelStatesModels(itemModel, type === 'data' ? undefined : rateLabel), { - labelFetcher: data.hostModel as FunnelSeriesModel, + labelFetcher: type === 'data' ? data.hostModel as FunnelSeriesModel : rateFetcher, labelDataIndex: idx, defaultOpacity: style.opacity, - defaultText: data.getName(idx) + defaultText: type === 'data' ? data.getName(idx) : layout.rate }, - { normal: { - align: labelLayout.textAlign, - verticalAlign: labelLayout.verticalAlign - } } + { + normal: { + align: labelLayout.textAlign, + verticalAlign: labelLayout.verticalAlign + } + } ); polygon.setTextConfig({ @@ -167,6 +265,9 @@ class FunnelPiece extends graphic.Polygon { stroke: visualColor }); } + + // relate ratePiece with dataPiece + ratePiece: FunnelPiece; } class FunnelView extends ChartView { @@ -182,26 +283,64 @@ class FunnelView extends ChartView { const oldData = this._data; const group = this.group; + // rate in other two mode did not support yet + const showRate = + seriesModel.get('showRate') + && !( + seriesModel.get('dynamicHeight') + || seriesModel.get('sort') === 'none' + ); data.diff(oldData) .add(function (idx) { - const funnelPiece = new FunnelPiece(data, idx); + const funnelPiece = new FunnelPiece(data, idx, 'data'); data.setItemGraphicEl(idx, funnelPiece); group.add(funnelPiece); + + if (showRate) { + const ratePiece = new FunnelPiece(data, idx, 'rate'); + group.add(ratePiece); + funnelPiece.ratePiece = ratePiece; + } }) .update(function (newIdx, oldIdx) { const piece = oldData.getItemGraphicEl(oldIdx) as FunnelPiece; - piece.updateData(data, newIdx); + piece.updateData(data, newIdx, 'data'); group.add(piece); data.setItemGraphicEl(newIdx, piece); + + // rate funnel piece may remove in this mount func + const ratePiece = piece.ratePiece; + if (showRate) { + if (ratePiece) { + ratePiece.updateData(data, newIdx, 'rate'); + group.add(ratePiece); + } + else { + const ratePiece = new FunnelPiece(data, newIdx, 'rate'); + group.add(ratePiece); + piece.ratePiece = ratePiece; + } + } + else { + if (ratePiece) { + graphic.removeElementWithFadeOut(ratePiece, seriesModel, oldIdx); + piece.ratePiece = null; + } + } }) .remove(function (idx) { - const piece = oldData.getItemGraphicEl(idx); + const piece = oldData.getItemGraphicEl(idx) as FunnelPiece; graphic.removeElementWithFadeOut(piece, seriesModel, idx); + + if (showRate) { + const ratePiece = piece.ratePiece; + graphic.removeElementWithFadeOut(ratePiece, seriesModel, idx); + } }) .execute(); @@ -213,7 +352,7 @@ class FunnelView extends ChartView { this._data = null; } - dispose() {} + dispose() { } } diff --git a/src/chart/funnel/funnelLayout.ts b/src/chart/funnel/funnelLayout.ts index 4d433c1b1e..335ee0d6f2 100644 --- a/src/chart/funnel/funnelLayout.ts +++ b/src/chart/funnel/funnelLayout.ts @@ -246,26 +246,78 @@ function labelLayout(data: SeriesData) { }); } +function rateLabelLayout(data: SeriesData) { + data.each(function (idx) { + const layout = data.getItemLayout(idx); + const points = layout.ratePoints; + + const isLabelInside = true; + + const textX = (points[0][0] + points[1][0] + points[2][0] + points[3][0]) / 4; + const textY = (points[0][1] + points[1][1] + points[2][1] + points[3][1]) / 4; + const textAlign = 'center'; + + const linePoints = [ + [textX, textY], [textX, textY] + ]; + + layout.rateLabel = { + linePoints: linePoints, + x: textX, + y: textY, + verticalAlign: 'middle', + textAlign: textAlign, + inside: isLabelInside + }; + }); +} + export default function funnelLayout(ecModel: GlobalModel, api: ExtensionAPI) { ecModel.eachSeriesByType('funnel', function (seriesModel: FunnelSeriesModel) { + // data about const data = seriesModel.getData(); const valueDim = data.mapDimension('value'); + const valueArr = data.mapArray(valueDim, function (val: number) { + return val; + }); + const valueSum = valueArr.reduce((pre, cur) => pre + cur); + // direction about const sort = seriesModel.get('sort'); - const viewRect = getViewRect(seriesModel, api); const orient = seriesModel.get('orient'); + + // size and pos about + const viewRect = getViewRect(seriesModel, api); const viewWidth = viewRect.width; const viewHeight = viewRect.height; - let indices = getSortedIndices(data, sort); let x = viewRect.x; let y = viewRect.y; - const sizeExtent = orient === 'horizontal' ? [ - parsePercent(seriesModel.get('minSize'), viewHeight), - parsePercent(seriesModel.get('maxSize'), viewHeight) - ] : [ - parsePercent(seriesModel.get('minSize'), viewWidth), - parsePercent(seriesModel.get('maxSize'), viewWidth) - ]; + let indices = getSortedIndices(data, sort); + + let gap = seriesModel.get('gap'); + const gapSum = gap * (data.count() - 1); + + // mapping mode about + const dynamicHeight = seriesModel.get('dynamicHeight'); + const showRate = seriesModel.get('showRate'); + // size extent based on orient and mapping mode + // determine the width extent of the funnel piece when dynamicHeight is false + // determine the height extent of the funnel piece when dynamicHeight if true + const isHorizontal = orient === 'horizontal'; + const size = dynamicHeight ? ( + isHorizontal ? viewWidth - gapSum : viewHeight - gapSum + ) : ( + isHorizontal ? viewHeight : viewWidth + ); + const sizeExtent = [ + parsePercent(seriesModel.get('minSize'), size), + size + ]; + if (!dynamicHeight) { + sizeExtent[1] = parsePercent(seriesModel.get('maxSize'), size); + } + + // data extent const dataExtent = data.getDataExtent(valueDim); let min = seriesModel.get('min'); let max = seriesModel.get('max'); @@ -276,16 +328,36 @@ export default function funnelLayout(ecModel: GlobalModel, api: ExtensionAPI) { max = dataExtent[1]; } + // determine the height of the funnel + let viewSize = dynamicHeight ? (isHorizontal ? viewHeight : viewWidth + ) : ( + isHorizontal ? viewWidth : viewHeight); + let itemSize = (viewSize - gapSum) / data.count(); + + if (dynamicHeight) { + viewSize = parsePercent(seriesModel.get('maxSize'), viewSize); + } + const funnelAlign = seriesModel.get('funnelAlign'); - let gap = seriesModel.get('gap'); - const viewSize = orient === 'horizontal' ? viewWidth : viewHeight; - let itemSize = (viewSize - gap * (data.count() - 1)) / data.count(); - const getLinePoints = function (idx: number, offset: number) { - // End point index is data.count() and we assign it 0 + // adjust related param + if (sort === 'ascending') { + // From bottom to top + itemSize = -itemSize; + gap = -gap; if (orient === 'horizontal') { - const val = data.get(valueDim, idx) as number || 0; - const itemHeight = linearMap(val, [min, max], sizeExtent, true); + x += viewWidth; + } + else { + y += viewHeight; + } + indices = indices.reverse(); + } + + const getLinePoints = function (offset: number, itemSize: number) { + // do not caculate line width in this func + if (orient === 'horizontal') { + const itemHeight = itemSize; let y0; switch (funnelAlign) { case 'top': @@ -304,8 +376,7 @@ export default function funnelLayout(ecModel: GlobalModel, api: ExtensionAPI) { [offset, y0 + itemHeight] ]; } - const val = data.get(valueDim, idx) as number || 0; - const itemWidth = linearMap(val, [min, max], sizeExtent, true); + const itemWidth = itemSize; let x0; switch (funnelAlign) { case 'left': @@ -324,68 +395,198 @@ export default function funnelLayout(ecModel: GlobalModel, api: ExtensionAPI) { ]; }; - if (sort === 'ascending') { - // From bottom to top - itemSize = -itemSize; - gap = -gap; - if (orient === 'horizontal') { - x += viewWidth; - } - else { - y += viewHeight; - } - indices = indices.reverse(); + const getItemSize = function (idx: number) { + const itemVal = data.get(valueDim, idx) as number || 0; + const itemSize = linearMap(itemVal, [min, max], sizeExtent, true); + return itemSize; + }; + + // exit shape control + const exitWidth = parsePercent(seriesModel.get('exitWidth'), 100); + + // dy height funnel piece about + let setDynamicHeightPoints: + ( + index: number, + idx: number, + pos: number, + pieceHeight: number + ) => void | null = null; + + if (dynamicHeight) { + setDynamicHeightPoints = (function () { + const dyminSize = parsePercent(seriesModel.get('minSize'), 100); + const dymaxSize = dyminSize < 100 ? sizeExtent[1] * 100 / (100 - dyminSize) : sizeExtent[1]; + let resSize = dymaxSize; + return function (index: number, idx: number, pos: number, pieceHeight: number) { + const start = getLinePoints(pos, resSize / dymaxSize * viewSize); + index === indices.length - 1 && exitWidth === 100 + || ( + resSize += sort === 'ascending' ? pieceHeight : -pieceHeight + ); + const end = getLinePoints(pos + pieceHeight, resSize / dymaxSize * viewSize); + + data.setItemLayout(idx, { + points: start.concat(end.slice().reverse()) + }); + }; + })(); } - for (let i = 0; i < indices.length; i++) { - const idx = indices[i]; - const nextIdx = indices[i + 1]; - const itemModel = data.getItemModel(idx); + // rate funnel about + let setRatePiecePoint: ( + index: number, + idx: number, + nextIdx: number, + pos: number, + pieceHeight: number + ) => void | null = null; - if (orient === 'horizontal') { - let width = itemModel.get(['itemStyle', 'width']); - if (width == null) { - width = itemSize; - } - else { - width = parsePercent(width, viewWidth); - if (sort === 'ascending') { - width = -width; + if (showRate) { + const getConverRate = (function () { + let firstVal: number; + let firstName: string; + let firstDataIndex: number; + // get rate fixed decimal places + const ratePrecision = seriesModel.get(['rateLabel', 'precision']); + const overallRatePrecision = seriesModel.get(['overallRateLabel', 'precision']); + return function (index: number, idx: number, nextIdx: number) { + const val = data.get(valueDim, idx) as number || 0; + const nextVal = data.get(valueDim, nextIdx) as number || 0; + let preName = data.getName(idx); + let nextName = data.getName(nextIdx); + let preDataIndex = idx; + let nextDataIndex = nextIdx; + let rate: number | string = nextVal / val; + rate = (rate * 100).toFixed(ratePrecision) + '%'; + if (index === 0) { + firstVal = val; + firstName = data.getName(idx); + firstDataIndex = idx; + } + else if (index === indices.length - 1) { + const lastVal = val; + rate = lastVal / firstVal; + rate = (rate * 100).toFixed(overallRatePrecision) + '%'; + nextName = preName; + preName = firstName; + preDataIndex = firstDataIndex; + nextDataIndex = idx; } + preDataIndex = preDataIndex + 1; + nextDataIndex = nextDataIndex + 1; + return { rate, nextName, preName, preDataIndex, nextDataIndex }; + }; + })(); + setRatePiecePoint = function ( + index: number, + idx: number, + nextIdx: number, + pos: number, + pieceHeight: number + ) { + // get this size + const itemSize = getItemSize(idx); + let exitSize = itemSize; + if (exitWidth != null && index === indices.length - 1) { + exitSize = itemSize * (exitWidth > 100 ? 100 : exitWidth) / 100; } + // data piece + const dataStart = getLinePoints(pos, itemSize); + const dataEnd = getLinePoints(pos + pieceHeight / 2, exitSize); + // rate piece + const nextSize = getItemSize(index === indices.length - 1 && exitWidth === 100 ? idx : nextIdx); - const start = getLinePoints(idx, x); - const end = getLinePoints(nextIdx, x + width); - - x += width + gap; + const rateStart = getLinePoints(pos + pieceHeight / 2, itemSize); + const rateEnd = getLinePoints(pos + pieceHeight, nextSize); + // rate string about + const { rate, nextName, preName, preDataIndex, nextDataIndex } = getConverRate(index, idx, nextIdx); data.setItemLayout(idx, { - points: start.concat(end.slice().reverse()) + points: dataStart.concat(dataEnd.slice().reverse()), + ratePoints: rateStart.concat(rateEnd.slice().reverse()), + isLastPiece: index === indices.length - 1, + rate, + nextName, + preName, + preDataIndex, + nextDataIndex }); + }; + + } + + // get the height of funnel piece + const getPieceHeight = function (pieceHeight: number | string, idx?: number): number { + // get funnel piece height pass to getLinePoints func based on data value + const val = data.get(valueDim, idx) as number || 0; + + if (dynamicHeight) { + // in dy height, user can't set itemHeight or itemWidth + pieceHeight = linearMap(val, [0, valueSum], [0, size], true); + pieceHeight = sort === 'ascending' ? -pieceHeight : pieceHeight; + return pieceHeight; + } + + // default mapping or show rate pieceHeight + if (pieceHeight == null) { + pieceHeight = itemSize; } else { - let height = itemModel.get(['itemStyle', 'height']); - if (height == null) { - height = itemSize; - } - else { - height = parsePercent(height, viewHeight); - if (sort === 'ascending') { - height = -height; - } - } + pieceHeight = parsePercent(pieceHeight, orient === 'horizontal' ? viewWidth : viewHeight); + pieceHeight = sort === 'ascending' ? -pieceHeight : pieceHeight; + } + return pieceHeight; + }; - const start = getLinePoints(idx, y); - const end = getLinePoints(nextIdx, y + height); + // set the line piont of the funnel piece + const setLayoutPoints = function ( + index: number, + idx: number, + nextIdx: number, + pieceHeight: number, + pos: number + ): void { + // The subsequent funnel shape modification will be done in this func. + // We don’t need to concern direction when we use this function to set points. + if (dynamicHeight) { + setDynamicHeightPoints(index, idx, pos, pieceHeight); + return; + } + else if (showRate && sort !== 'none') { + setRatePiecePoint(index, idx, nextIdx, pos, pieceHeight); + return; + } + const start = getLinePoints(pos, getItemSize(idx)); + // get end line width; + const nIdx = index === indices.length - 1 && exitWidth === 100 ? idx : nextIdx; + const end = getLinePoints(pos + pieceHeight, getItemSize(nIdx)); - y += height + gap; + data.setItemLayout(idx, { + points: start.concat(end.slice().reverse()) + }); + }; - data.setItemLayout(idx, { - points: start.concat(end.slice().reverse()) - }); + for (let i = 0; i < indices.length; i++) { + const idx = indices[i]; + const nextIdx = indices[i + 1]; + const itemModel = data.getItemModel(idx); + + if (orient === 'horizontal') { + const width = getPieceHeight(itemModel.get(['itemStyle', 'width']), idx); + setLayoutPoints(i, idx, nextIdx, width, x); + x += width + gap; + } + else { + const height = getPieceHeight(itemModel.get(['itemStyle', 'height']), idx); + setLayoutPoints(i, idx, nextIdx, height, y); + y += height + gap; } } labelLayout(data); + if (showRate && !dynamicHeight && sort !== 'none') { + rateLabelLayout(data); + } }); } diff --git a/test/funnel-dynamicHeight.html b/test/funnel-dynamicHeight.html new file mode 100644 index 0000000000..0592d57175 --- /dev/null +++ b/test/funnel-dynamicHeight.html @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/test/funnel-showRate.html b/test/funnel-showRate.html new file mode 100644 index 0000000000..a66a77af03 --- /dev/null +++ b/test/funnel-showRate.html @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json index c08e7ae77e..e4398c5596 100644 --- a/test/runTest/actions/__meta__.json +++ b/test/runTest/actions/__meta__.json @@ -89,6 +89,8 @@ "emphasis-disabled": 13, "emphasis-inherit": 1, "funnel": 2, + "funnel-dynamicHeight": 1, + "funnel-showRate": 1, "gauge-simple": 2, "geo-map": 4, "geo-map-features": 3, diff --git a/test/runTest/actions/funnel-dynamicHeight.json b/test/runTest/actions/funnel-dynamicHeight.json new file mode 100644 index 0000000000..5ef7660003 --- /dev/null +++ b/test/runTest/actions/funnel-dynamicHeight.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":165,"x":645,"y":243},{"type":"mousemove","time":365,"x":711,"y":59},{"type":"mousemove","time":572,"x":707,"y":29},{"type":"mousemove","time":781,"x":703,"y":9},{"type":"mousemove","time":987,"x":703,"y":8},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string>div>div.c>select","value":"descending","time":1903,"target":"select"},{"time":1904,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1910,"x":692,"y":22},{"type":"mousemove","time":2112,"x":689,"y":16},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string>div>div.c>select","value":"none","time":3097,"target":"select"},{"time":3098,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3105,"x":684,"y":32},{"type":"mousemove","time":3305,"x":681,"y":16},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string>div>div.c>select","value":"ascending","time":4746,"target":"select"},{"time":4747,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4754,"x":668,"y":53},{"type":"mousemove","time":4959,"x":667,"y":48},{"type":"mousemove","time":5173,"x":667,"y":47},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string:nth-child(2)>div>div.c>select","value":"horizontal","time":5881,"target":"select"},{"time":5882,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5900,"x":666,"y":38},{"type":"mousemove","time":6105,"x":666,"y":37},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string:nth-child(2)>div>div.c>select","value":"vertical","time":7037,"target":"select"},{"time":7038,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7097,"x":663,"y":63},{"type":"mousemove","time":7297,"x":663,"y":67},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string:nth-child(3)>div>div.c>select","value":"false","time":8180,"target":"select"},{"time":8181,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8198,"x":665,"y":73},{"type":"mousemove","time":8404,"x":664,"y":70},{"type":"mousemove","time":8631,"x":664,"y":68},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string:nth-child(3)>div>div.c>select","value":"true","time":9421,"target":"select"},{"time":9422,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9429,"x":672,"y":108},{"type":"mousemove","time":9631,"x":702,"y":100},{"type":"mousemove","time":9839,"x":711,"y":95},{"type":"mousedown","time":9962,"x":716,"y":96},{"type":"mousemove","time":10056,"x":720,"y":96},{"type":"mousemove","time":10265,"x":736,"y":98},{"type":"mouseup","time":10310,"x":738,"y":97},{"time":10311,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10473,"x":733,"y":96},{"type":"mousemove","time":10694,"x":717,"y":97},{"type":"mousemove","time":10831,"x":717,"y":97},{"type":"mousedown","time":10937,"x":720,"y":98},{"type":"mousemove","time":11050,"x":699,"y":97},{"type":"mousemove","time":11262,"x":639,"y":89},{"type":"mouseup","time":11455,"x":617,"y":92},{"time":11456,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11471,"x":617,"y":92},{"type":"mousemove","time":11680,"x":678,"y":110},{"type":"mousemove","time":11881,"x":678,"y":111},{"type":"mousemove","time":12081,"x":682,"y":128},{"type":"mousedown","time":12155,"x":682,"y":128},{"type":"mouseup","time":12251,"x":683,"y":127},{"time":12252,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12291,"x":683,"y":127},{"type":"mousemove","time":12497,"x":660,"y":127},{"type":"mousemove","time":12697,"x":656,"y":126},{"type":"mousemove","time":12898,"x":705,"y":132},{"type":"mousedown","time":12989,"x":711,"y":133},{"type":"mouseup","time":13089,"x":713,"y":133},{"time":13090,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13104,"x":713,"y":133},{"type":"mousemove","time":13313,"x":700,"y":143},{"type":"mousemove","time":13513,"x":680,"y":159},{"type":"mousedown","time":13573,"x":678,"y":159},{"type":"mouseup","time":13670,"x":677,"y":158},{"time":13671,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13713,"x":671,"y":158},{"type":"mousemove","time":13913,"x":662,"y":159},{"type":"mousemove","time":14114,"x":658,"y":157},{"type":"mousemove","time":14321,"x":658,"y":156},{"type":"mousedown","time":14455,"x":663,"y":152},{"type":"mouseup","time":14529,"x":663,"y":152},{"time":14530,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14544,"x":663,"y":152},{"type":"mousemove","time":14746,"x":678,"y":151},{"type":"mousemove","time":14954,"x":679,"y":151},{"type":"mousemove","time":15183,"x":497,"y":27},{"type":"mousemove","time":15388,"x":458,"y":14},{"type":"mousedown","time":15518,"x":456,"y":17},{"type":"mouseup","time":15577,"x":455,"y":17},{"time":15578,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15626,"x":455,"y":17},{"type":"mousemove","time":15814,"x":455,"y":17},{"type":"mousemove","time":16014,"x":454,"y":12},{"type":"mousedown","time":16089,"x":454,"y":12},{"type":"mouseup","time":16155,"x":454,"y":12},{"time":16156,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16229,"x":529,"y":12},{"type":"mousemove","time":16430,"x":541,"y":12},{"type":"mousemove","time":16643,"x":522,"y":12},{"type":"mousedown","time":16701,"x":522,"y":12},{"type":"mouseup","time":16757,"x":522,"y":12},{"time":16758,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":17357,"x":522,"y":12},{"type":"mouseup","time":17427,"x":522,"y":12},{"time":17428,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":17672,"x":521,"y":12},{"type":"mousemove","time":17874,"x":650,"y":156},{"type":"mousemove","time":18083,"x":603,"y":517},{"type":"mousemove","time":18287,"x":572,"y":578}],"scrollY":94,"scrollX":0,"timestamp":1666345765764}] \ No newline at end of file diff --git a/test/runTest/actions/funnel-showRate.json b/test/runTest/actions/funnel-showRate.json new file mode 100644 index 0000000000..6770de15c6 --- /dev/null +++ b/test/runTest/actions/funnel-showRate.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":216,"x":782,"y":348},{"type":"mousemove","time":416,"x":670,"y":80},{"type":"mousemove","time":621,"x":664,"y":4},{"type":"mousemove","time":783,"x":667,"y":0},{"type":"mousemove","time":988,"x":671,"y":10},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string>div>div.c>select","value":"descending","time":2289,"target":"select"},{"time":2290,"delay":400,"type":"screenshot-auto"},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string>div>div.c>select","value":"none","time":3871,"target":"select"},{"time":3872,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4015,"x":687,"y":17},{"type":"mousemove","time":4225,"x":687,"y":15},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string>div>div.c>select","value":"descending","time":5598,"target":"select"},{"time":5599,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5608,"x":698,"y":16},{"type":"mousemove","time":5811,"x":691,"y":28},{"type":"mousemove","time":6015,"x":689,"y":37},{"type":"mousemove","time":6248,"x":689,"y":43},{"type":"mousemove","time":6482,"x":689,"y":43},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string:nth-child(2)>div>div.c>select","value":"horizontal","time":7640,"target":"select"},{"time":7641,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7715,"x":689,"y":45},{"type":"mousemove","time":7922,"x":684,"y":63},{"type":"mousemove","time":8131,"x":680,"y":70},{"type":"mousemove","time":8336,"x":670,"y":71},{"type":"mousedown","time":8380,"x":669,"y":71},{"type":"mouseup","time":8441,"x":669,"y":71},{"time":8442,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8580,"x":669,"y":71},{"type":"mousemove","time":8664,"x":668,"y":71},{"type":"mousemove","time":8864,"x":667,"y":72},{"type":"mousedown","time":9057,"x":690,"y":69},{"type":"mousemove","time":9093,"x":690,"y":69},{"type":"mouseup","time":9141,"x":690,"y":69},{"time":9142,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9299,"x":695,"y":69},{"type":"mousedown","time":9499,"x":703,"y":69},{"type":"mousemove","time":9519,"x":704,"y":69},{"type":"mouseup","time":9609,"x":704,"y":69},{"time":9610,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9737,"x":698,"y":56},{"type":"mousedown","time":9923,"x":696,"y":53},{"type":"mousemove","time":9941,"x":696,"y":53},{"type":"mouseup","time":9978,"x":695,"y":51},{"time":9979,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10148,"x":694,"y":43},{"type":"mousemove","time":10367,"x":694,"y":40},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string:nth-child(2)>div>div.c>select","value":"vertical","time":11205,"target":"select"},{"time":11206,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11214,"x":683,"y":86},{"type":"mousemove","time":11414,"x":680,"y":92},{"type":"mousemove","time":11626,"x":675,"y":96},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string:nth-child(4)>div>div.c>select","value":"false","time":12850,"target":"select"},{"time":12851,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12883,"x":680,"y":113},{"type":"mousemove","time":13083,"x":677,"y":106},{"type":"mousemove","time":13289,"x":674,"y":98},{"type":"mousemove","time":13507,"x":671,"y":89},{"type":"valuechange","selector":"div.dg.ac>div.dg.main.a>ul>li.cr.string:nth-child(4)>div>div.c>select","value":"true","time":14474,"target":"select"},{"time":14475,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14482,"x":671,"y":228},{"type":"mousemove","time":14682,"x":670,"y":357},{"type":"mousemove","time":14893,"x":664,"y":367},{"type":"mousemove","time":15099,"x":461,"y":62},{"type":"mousemove","time":15311,"x":458,"y":16},{"type":"mousemove","time":15515,"x":451,"y":1},{"type":"mousedown","time":15714,"x":450,"y":8},{"type":"mousemove","time":15740,"x":450,"y":8},{"type":"mouseup","time":15777,"x":450,"y":8},{"time":15778,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16316,"x":450,"y":9},{"type":"mousedown","time":16509,"x":450,"y":10},{"type":"mousemove","time":16523,"x":450,"y":10},{"type":"mouseup","time":16577,"x":450,"y":10},{"time":16578,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16733,"x":450,"y":10},{"type":"mousemove","time":16932,"x":481,"y":10},{"type":"mousemove","time":17132,"x":494,"y":7},{"type":"mousedown","time":17313,"x":504,"y":7},{"type":"mousemove","time":17392,"x":504,"y":7},{"type":"mouseup","time":17418,"x":504,"y":7},{"time":17419,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":18109,"x":504,"y":7},{"type":"mouseup","time":18178,"x":504,"y":7},{"time":18179,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":18398,"x":504,"y":8},{"type":"mousemove","time":18599,"x":399,"y":20},{"type":"mousemove","time":18809,"x":307,"y":20},{"type":"mousemove","time":19015,"x":277,"y":14},{"type":"mousedown","time":19226,"x":257,"y":12},{"type":"mousemove","time":19242,"x":257,"y":12},{"type":"mouseup","time":19314,"x":257,"y":12},{"time":19315,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":20194,"x":257,"y":12},{"type":"mouseup","time":20282,"x":257,"y":12},{"time":20283,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":20847,"x":259,"y":11},{"type":"mousemove","time":21048,"x":426,"y":376},{"type":"mousemove","time":21259,"x":430,"y":592}],"scrollY":166,"scrollX":0,"timestamp":1666345365626}] \ No newline at end of file