Skip to content

Commit

Permalink
fix(#2124): fix funnel crash when data contains null, 0 (#2138)
Browse files Browse the repository at this point in the history
* fix(#2124): fix funnel crash when data contains null, 0

* chore: fix ci

* chore: fix function name typo

* fix: typo
  • Loading branch information
hustcc authored Dec 23, 2020
1 parent 5d41021 commit a318310
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 56 deletions.
95 changes: 95 additions & 0 deletions __tests__/bugs/issue-2124-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Funnel } from '../../src';
import { createDiv } from '../utils/dom';

const DATA1 = [
{ stage: '简历筛选', number: 253 },
{ stage: '初试人数', number: 151 },
{ stage: '复试人数', number: 0 },
{ stage: '录取人数', number: 87 },
{ stage: '入职人数', number: null },
{ stage: '离职人数', number: 10 },
];

const DATA2 = [
{ stage: '简历筛选', number: 253, company: 'A公司' },
{ stage: '初试人数', number: 151, company: 'A公司' },
{ stage: '复试人数', number: 0, company: 'A公司' },
{ stage: '录取人数', number: 87, company: 'A公司' },
{ stage: '入职人数', number: null, company: 'A公司' },
{ stage: '离职人数', number: 10, company: 'A公司' },
{ stage: '简历筛选', number: 303, company: 'B公司' },
{ stage: '初试人数', number: 0, company: 'B公司' },
{ stage: '复试人数', number: 153, company: 'B公司' },
{ stage: '录取人数', number: 117, company: 'B公司' },
{ stage: '入职人数', number: 79, company: 'B公司' },
{ stage: '离职人数', number: 15, company: 'B公司' },
];

describe('#2124', () => {
it('Funnel 数据为 null, 0 不能报错', async () => {
const plot = new Funnel(createDiv(), {
data: DATA1,
xField: 'stage',
yField: 'number',
legend: false,
minSize: 0.1,
});

plot.render();

expect(
plot.chart
.getController('annotation')
.getComponents()
.map((co) => co.component.cfg.text.content)
).toEqual(['转化率: 59.68%', '转化率: -∞', '转化率: ∞', '转化率: -', '转化率: -']);

plot.destroy();
});

it('对称漏斗图', () => {
const plot = new Funnel(createDiv(), {
data: DATA2,
xField: 'stage',
yField: 'number',
compareField: 'company',
meta: {
stage: {
alias: '行为',
},
pv: {
alias: '人数',
formatter: (v) => `${v}次`,
},
},
tooltip: {
fields: ['stage', 'number', 'company'],
formatter: (v) => ({
name: `${v.company}${v.stage}`,
value: v.number,
}),
},
legend: false,
});

plot.render();

expect(
plot.chart.views[0]
.getController('annotation')
.getComponents()
.filter((co) => co.component.cfg.type === 'line')
.map((co) => co.component.cfg.text.content)
).toEqual(['转化率: 59.68%', '转化率: -∞', '转化率: ∞', '转化率: -', '转化率: -']);

expect(
plot.chart.views[1]
.getController('annotation')
.getComponents()
.filter((co) => co.component.cfg.type === 'line')
.map((co) => co.component.cfg.text.content)
).toEqual(['转化率: -∞', '转化率: ∞', '转化率: 76.47%', '转化率: 67.52%', '转化率: 18.99%']);

plot.destroy();
});
});
3 changes: 2 additions & 1 deletion __tests__/unit/plots/funnel/basic-spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { get } from '@antv/util';
import { Funnel } from '../../../../src';
import { PV_DATA } from '../../../data/conversion';
import { createDiv } from '../../../utils/dom';
Expand Down Expand Up @@ -65,7 +66,7 @@ describe('basic funnel', () => {
data.forEach((item, index) => {
expect(item[FUNNEL_PERCENT]).toEqual(item.pv / data[0].pv);
expect(item[FUNNEL_MAPPING_VALUE]).toEqual((item.pv / data[0].pv) * 0.5 + 0.3);
expect(item[FUNNEL_CONVERSATION]).toEqual(index === 0 ? 1 : item.pv / data[index - 1].pv);
expect(item[FUNNEL_CONVERSATION]).toEqual([get(data, [index - 1, 'pv']), item.pv]);
});
});
});
Expand Down
4 changes: 2 additions & 2 deletions __tests__/unit/plots/funnel/compare-spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { maxBy } from '@antv/util';
import { get, maxBy } from '@antv/util';
import { Funnel } from '../../../../src';
import { PV_DATA_COMPARE } from '../../../data/conversion';
import { createDiv } from '../../../utils/dom';
Expand Down Expand Up @@ -77,7 +77,7 @@ describe('compare funnel', () => {
const percent = item.pv / max;
expect(item[FUNNEL_PERCENT]).toEqual(percent);
expect(item[FUNNEL_MAPPING_VALUE]).toEqual(0.5 * percent + 0.3);
expect(item[FUNNEL_CONVERSATION]).toEqual(originIndex === 0 ? 1 : item.pv / originData[originIndex - 1].pv);
expect(item[FUNNEL_CONVERSATION]).toEqual([get(originData, [originIndex - 1, 'pv']), item.pv]);
});
});
});
Expand Down
59 changes: 34 additions & 25 deletions __tests__/unit/plots/funnel/conversion-tag-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ describe('conversition tag', () => {

const annotation = funnel.chart.getController('annotation').getComponents();
expect(annotation.length).toEqual(4);
PV_DATA.forEach((pvItem, index) => {
if (index === 0) return;
const content = annotation[index - 1].component.get('text').content;
expect(content).toBe(`转化率${(pvItem[FUNNEL_CONVERSATION] * 100).toFixed(2)}%`);
});

expect(annotation.map((co) => co.component.get('text').content)).toEqual([
'转化率: 70.00%',
'转化率: 71.43%',
'转化率: 60.00%',
'转化率: 56.67%',
]);

// 自定义 label
funnel.update({
Expand All @@ -39,14 +41,17 @@ describe('conversition tag', () => {

const customAnnotation = funnel.chart.getController('annotation').getComponents();
expect(customAnnotation.length).toEqual(4);
PV_DATA.forEach((pvItem, index) => {
if (index === 0) return;
const text = customAnnotation[index - 1].component.get('text');
expect(text.content).toBe(`${pvItem[FUNNEL_PERCENT]}占比`);
expect(text.offsetX).toBe(50);
expect(text.offsetY).toBe(20);
expect(text.style.fontSize).toBe(18);
});

expect(customAnnotation[0].component.get('text').offsetX).toBe(50);
expect(customAnnotation[0].component.get('text').offsetY).toBe(20);
expect(customAnnotation[0].component.get('text').style.fontSize).toBe(18);

expect(customAnnotation.map((co) => co.component.get('text').content)).toEqual([
'0.7占比',
'0.5占比',
'0.3占比',
'0.17占比',
]);

// 关闭转化率组件
funnel.update({
Expand All @@ -70,18 +75,22 @@ describe('conversition tag', () => {
});
funnel.render();

funnel.chart.views.forEach((funnelView) => {
const { data } = funnelView.getOptions();
const annotation = funnelView.getController('annotation').getComponents();
expect(annotation.length).toEqual(5);
expect(annotation[0].component.cfg.content).toBe(data[0].quarter);
data.forEach((pvItem, index) => {
if (index === 0) return;
expect(annotation[index].component.get('text').content).toBe(
`转化率${(pvItem[FUNNEL_CONVERSATION] * 100).toFixed(2)}%`
);
});
});
const [v1, v2] = funnel.chart.views;
const annotation1 = v1.getController('annotation').getComponents();
const d1 = v1.getOptions().data;
expect(annotation1[0].component.cfg.content).toBe(d1[0].quarter);

expect(
annotation1.filter((co) => co.component.get('type') === 'line').map((co) => co.component.get('text').content)
).toEqual(['转化率: 70.00%', '转化率: 71.43%', '转化率: 60.00%', '转化率: 76.67%']);

const annotation2 = v2.getController('annotation').getComponents();
const d2 = v2.getOptions().data;
expect(annotation2[0].component.cfg.content).toBe(d2[0].quarter);

expect(
annotation2.filter((co) => co.component.get('type') === 'line').map((co) => co.component.get('text').content)
).toEqual(['转化率: 78.75%', '转化率: 74.60%', '转化率: 51.06%', '转化率: 72.92%']);

funnel.destroy();
});
Expand Down
3 changes: 2 additions & 1 deletion __tests__/unit/plots/funnel/dynamic-height-spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { get } from '@antv/util';
import { Funnel } from '../../../../src';
import { PV_DATA } from '../../../data/conversion';
import { createDiv } from '../../../utils/dom';
Expand Down Expand Up @@ -55,7 +56,7 @@ describe('dynamicHeight funnel', () => {
data.forEach((item, index) => {
expect(item[PLOYGON_Y][0] - item[PLOYGON_Y][2]).toEqual(item[FUNNEL_TOTAL_PERCENT]);
expect(item[FUNNEL_PERCENT]).toEqual(item.pv / data[0].pv);
expect(item[FUNNEL_CONVERSATION]).toEqual(index === 0 ? 1 : item.pv / data[index - 1].pv);
expect(item[FUNNEL_CONVERSATION]).toEqual([get(data, [index - 1, 'pv']), item.pv]);
});

// color
Expand Down
4 changes: 2 additions & 2 deletions __tests__/unit/plots/funnel/facet-spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { maxBy } from '@antv/util';
import { maxBy, get } from '@antv/util';
import { Funnel } from '../../../../src';
import { PV_DATA_COMPARE } from '../../../data/conversion';
import { createDiv } from '../../../utils/dom';
Expand Down Expand Up @@ -75,7 +75,7 @@ describe('facet funnel', () => {
const percent = item.pv / max;
expect(item[FUNNEL_PERCENT]).toEqual(percent);
expect(item[FUNNEL_MAPPING_VALUE]).toEqual(0.5 * percent + 0.3);
expect(item[FUNNEL_CONVERSATION]).toEqual(originIndex === 0 ? 1 : item.pv / originData[originIndex - 1].pv);
expect(item[FUNNEL_CONVERSATION]).toEqual([get(originData, [originIndex - 1, 'pv']), item.pv]);
});
});
});
Expand Down
34 changes: 17 additions & 17 deletions __tests__/unit/utils/conversion-spec.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { conversionTagformatter } from '../../../src/utils/conversion';
import { conversionTagFormatter } from '../../../src/utils/conversion';

describe('conversionTagformatter', () => {
it('conversionTagformatter', () => {
expect(conversionTagformatter(0, 0)).toBe('0.00%');
expect(conversionTagformatter(0, 1)).toBe('∞');
expect(conversionTagformatter(1, 0)).toBe('-∞');
expect(conversionTagformatter(10, 20)).toBe('200.00%');
expect(conversionTagformatter(40, 20)).toBe('50.00%');
describe('conversionTagFormatter', () => {
it('conversionTagFormatter', () => {
expect(conversionTagFormatter(0, 0)).toBe('0.00%');
expect(conversionTagFormatter(0, 1)).toBe('∞');
expect(conversionTagFormatter(1, 0)).toBe('-∞');
expect(conversionTagFormatter(10, 20)).toBe('200.00%');
expect(conversionTagFormatter(40, 20)).toBe('50.00%');

expect(conversionTagformatter(null, 20)).toBe('-');
expect(conversionTagformatter(20, null)).toBe('-');
expect(conversionTagformatter(null, null)).toBe('-');
expect(conversionTagformatter(undefined, 20)).toBe('-');
expect(conversionTagformatter(20, undefined)).toBe('-');
expect(conversionTagformatter(undefined, undefined)).toBe('-');
expect(conversionTagFormatter(null, 20)).toBe('-');
expect(conversionTagFormatter(20, null)).toBe('-');
expect(conversionTagFormatter(null, null)).toBe('-');
expect(conversionTagFormatter(undefined, 20)).toBe('-');
expect(conversionTagFormatter(20, undefined)).toBe('-');
expect(conversionTagFormatter(undefined, undefined)).toBe('-');

// @ts-ignore
expect(conversionTagformatter(false, 20)).toBe('-');
expect(conversionTagFormatter(false, 20)).toBe('-');
// @ts-ignore
expect(conversionTagformatter(20, 'wef')).toBe('-');
expect(conversionTagFormatter(20, 'wef')).toBe('-');
// @ts-ignore
expect(conversionTagformatter(30, {})).toBe('-');
expect(conversionTagFormatter(30, {})).toBe('-');
});
});
4 changes: 2 additions & 2 deletions src/adaptor/conversion-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { map, find, each, isObject } from '@antv/util';
import { Coordinate, IGroup, ShapeAttrs, Element, Geometry, View, getTheme } from '@antv/g2';
import { Params } from '../core/adaptor';
import { deepAssign } from '../utils';
import { conversionTagformatter } from '../utils/conversion';
import { conversionTagFormatter } from '../utils/conversion';
import { Options } from '../types';

/** 转化率组件配置选项 */
Expand Down Expand Up @@ -74,7 +74,7 @@ function getConversionTagOptionsWithDefaults(options: ConversionTagOptions, hori
textAlign: 'center',
textBaseline: 'middle',
},
formatter: conversionTagformatter,
formatter: conversionTagFormatter,
},
},
options
Expand Down
4 changes: 3 additions & 1 deletion src/plots/funnel/adaptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Params } from '../../core/adaptor';
import { interaction, animation, theme, scale, annotation, tooltip } from '../../adaptor/common';
import { flow, deepAssign } from '../../utils';
import { conversionTagFormatter } from '../../utils/conversion';
import { FunnelOptions } from './types';
import { basicFunnel } from './geometries/basic';
import { compareFunnel } from './geometries/compare';
Expand Down Expand Up @@ -66,7 +67,8 @@ function defaultOptions(params: Params<FunnelOptions>): Params<FunnelOptions> {
offsetX: 10,
offsetY: 0,
style: {},
formatter: (datum) => `转化率${(datum[FUNNEL_CONVERSATION] * 100).toFixed(2)}%`,
// conversionTag 的计算和显示逻辑统一保持一致
formatter: (datum) => `转化率: ${conversionTagFormatter(...(datum[FUNNEL_CONVERSATION] as [number, number]))}`,
},
};

Expand Down
5 changes: 3 additions & 2 deletions src/plots/funnel/geometries/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Types } from '@antv/g2';
import { isFunction, map, isNumber, maxBy } from '@antv/util';
import { isFunction, map, isNumber, maxBy, get } from '@antv/util';
import { Datum, Data } from '../../../types/common';
import { FUNNEL_PERCENT, FUNNEL_CONVERSATION, FUNNEL_MAPPING_VALUE } from '../constant';
import { Params } from '../../../core/adaptor';
Expand All @@ -26,7 +26,8 @@ export function transformData(
const percent = row[yField] / maxYFieldValue;
row[FUNNEL_PERCENT] = percent;
row[FUNNEL_MAPPING_VALUE] = (max - min) * percent + min;
row[FUNNEL_CONVERSATION] = index === 0 ? 1 : row[yField] / data[index - 1][yField];
// 转化率数据存储前后数据
row[FUNNEL_CONVERSATION] = [get(data, [index - 1, yField]), row[yField]];
}
return row;
});
Expand Down
4 changes: 2 additions & 2 deletions src/plots/funnel/geometries/dynamic-height.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { map, reduce, maxBy, isArray } from '@antv/util';
import { map, reduce, maxBy, isArray, get } from '@antv/util';
import { Types } from '@antv/g2';
import { flow } from '../../../utils';
import { Params } from '../../../core/adaptor';
Expand Down Expand Up @@ -70,7 +70,7 @@ function field(params: Params<FunnelOptions>): Params<FunnelOptions> {
row[PLOYGON_X] = x;
row[PLOYGON_Y] = y;
row[FUNNEL_PERCENT] = row[yField] / max;
row[FUNNEL_CONVERSATION] = index === 0 ? 1 : row[yField] / data[index - 1][yField];
row[FUNNEL_CONVERSATION] = [get(data, [index - 1, yField]), row[yField]];
return row;
});

Expand Down
2 changes: 1 addition & 1 deletion src/utils/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { isNumber } from '@antv/util';
* @param prev
* @param next
*/
export function conversionTagformatter(prev: number, next: number): string {
export function conversionTagFormatter(prev: number, next: number): string {
if (!isNumber(prev) || !isNumber(next)) {
return '-';
}
Expand Down

0 comments on commit a318310

Please sign in to comment.