Skip to content

Commit 654c8e1

Browse files
authored
fix: Scatter regression line and Dual Axes change data (#1848)
* fix: fix Scatter regression line and Dual Axes change data * fix: converage
1 parent 8f035d8 commit 654c8e1

File tree

10 files changed

+355
-151
lines changed

10 files changed

+355
-151
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { DualAxes } from '../../../../src';
2+
import { PV_DATA, UV_DATA, PV_DATA_MULTI, UV_DATA_MULTI } from '../../../data/pv-uv';
3+
import {} from '../../../data/pv-uv';
4+
import { createDiv } from '../../../utils/dom';
5+
import { LEFT_AXES_VIEW, RIGHT_AXES_VIEW } from '../../../../src/plots/dual-axes/constant';
6+
import { findViewById } from '../../../../src/utils/view';
7+
8+
describe('Line-Column', () => {
9+
it('Line-Colomn', () => {
10+
const dualAxes = new DualAxes(createDiv(), {
11+
height: 500,
12+
data: [[], []],
13+
xField: 'date',
14+
yField: ['pv', 'uv'],
15+
geometryOptions: [
16+
{
17+
geometry: 'line',
18+
connectNulls: false,
19+
smooth: true,
20+
color: '#f00',
21+
},
22+
{
23+
geometry: 'column',
24+
},
25+
],
26+
});
27+
28+
dualAxes.render();
29+
// 先柱后线
30+
expect(dualAxes.chart.views[0].id).toBe(RIGHT_AXES_VIEW);
31+
expect(dualAxes.chart.views[1].id).toBe(LEFT_AXES_VIEW);
32+
dualAxes.changeData([PV_DATA, UV_DATA]);
33+
const leftView = findViewById(dualAxes.chart, LEFT_AXES_VIEW);
34+
const rightView = findViewById(dualAxes.chart, RIGHT_AXES_VIEW);
35+
// line
36+
const leftGeometry = leftView.geometries.find((g) => g.type === 'line');
37+
const rightGeometry = rightView.geometries.find((g) => g.type === 'interval');
38+
// @ts-ignore
39+
expect(leftGeometry.shapeType).toBe('line');
40+
expect(rightGeometry.shapeType).toBe('interval');
41+
dualAxes.destroy();
42+
});
43+
44+
it('stack column and mutilpe line', () => {
45+
const dualAxes = new DualAxes(createDiv('test DualAxes change data'), {
46+
width: 400,
47+
height: 500,
48+
data: [[], []],
49+
xField: 'date',
50+
yField: ['pv', 'uv'],
51+
geometryOptions: [
52+
{
53+
geometry: 'column',
54+
seriesField: 'site',
55+
isStack: true,
56+
},
57+
{
58+
geometry: 'line',
59+
seriesField: 'site',
60+
isStack: true,
61+
point: {
62+
style: () => ({ fill: 'red' }),
63+
},
64+
},
65+
],
66+
});
67+
68+
dualAxes.render();
69+
expect(dualAxes.chart.getOptions().data.length).toBe(0);
70+
dualAxes.changeData([PV_DATA_MULTI, UV_DATA_MULTI]);
71+
const element = dualAxes.chart.views[1].geometries[1].elements[0];
72+
expect(element.getModel().shape).toBe('circle');
73+
expect(element.getData().site).toBeDefined();
74+
expect(element.getModel().style.fill).toBe('red');
75+
});
76+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Histogram } from '../../../../src';
2+
import { histogramData } from '../../../data/histogram-data';
3+
import { createDiv } from '../../../utils/dom';
4+
5+
describe('Histogram: change data', () => {
6+
const histogram = new Histogram(createDiv(), {
7+
width: 400,
8+
height: 300,
9+
appendPadding: 10,
10+
data: [],
11+
binField: 'value',
12+
binWidth: 2,
13+
tooltip: {
14+
title: 'hello wold!',
15+
},
16+
});
17+
18+
histogram.render();
19+
20+
it('change data', () => {
21+
// @ts-ignore
22+
expect(histogram.chart.options.tooltip.title).toBe('hello wold!');
23+
histogram.changeData(histogramData);
24+
expect(histogram.chart.getData().length).toBe(12);
25+
});
26+
});

__tests__/unit/plots/scatter/legend-spec.ts

+57
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,61 @@ describe('scatter', () => {
8080
expect(legendController.getComponents()[0].id).toBe('legend-gender');
8181
expect(legendController.getComponents()[0].direction).toBe('top-right');
8282
});
83+
84+
it('legend: legend * sizeField * false', () => {
85+
const scatter = new Scatter(createDiv(), {
86+
width: 400,
87+
height: 300,
88+
appendPadding: 10,
89+
data,
90+
xField: 'weight',
91+
yField: 'height',
92+
shapeField: 'gender',
93+
size: [2, 8],
94+
sizeField: 'weight',
95+
color: ['red', 'blue'],
96+
colorField: 'gender',
97+
xAxis: {
98+
nice: true,
99+
},
100+
legend: false,
101+
});
102+
103+
scatter.render();
104+
const legendController = scatter.chart.getController('legend');
105+
// @ts-ignore
106+
const { option } = legendController;
107+
expect(option).toBe(false);
108+
});
109+
110+
it('legend: legend * sizeField * true', () => {
111+
const scatter = new Scatter(createDiv(), {
112+
width: 400,
113+
height: 300,
114+
appendPadding: 10,
115+
data,
116+
xField: 'weight',
117+
yField: 'height',
118+
shapeField: 'gender',
119+
size: [2, 8],
120+
sizeField: 'weight',
121+
color: ['red', 'blue'],
122+
colorField: 'gender',
123+
xAxis: {
124+
nice: true,
125+
},
126+
legend: {},
127+
});
128+
129+
scatter.render();
130+
const legendController = scatter.chart.getController('legend');
131+
// @ts-ignore
132+
const {
133+
// @ts-ignore
134+
option: { weight, height, gender },
135+
} = legendController;
136+
expect(weight).toBe(false);
137+
expect(height).toBe(false);
138+
expect(gender).toBeTruthy();
139+
});
83140
});

__tests__/unit/plots/scatter/regression-line-spec.ts

+52-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Scatter } from '../../../../src';
22
import { createDiv } from '../../../utils/dom';
3+
import { delay } from '../../../utils/delay';
34

45
const data = [
56
{ x: 1, y: 4.181 },
@@ -72,7 +73,7 @@ describe('scatter', () => {
7273
expect(elements[0].getModel().size).toBe(5);
7374
});
7475

75-
it('regressionLine: algorithm', () => {
76+
it('regressionLine: algorithm', async () => {
7677
const scatter = new Scatter(createDiv('regressionLine*algorithm'), {
7778
data,
7879
width: 400,
@@ -86,16 +87,18 @@ describe('scatter', () => {
8687
lineWidth: 1,
8788
fill: '#5B8FF9',
8889
},
90+
animation: false,
8991
regressionLine: {
9092
algorithm: [
91-
[0, 0],
92-
[200, 200],
93+
[8, 6],
94+
[16, 7],
95+
[24, 7],
9396
],
9497
},
9598
});
9699

97100
scatter.render();
98-
101+
await delay(500);
99102
const geometry = scatter.chart.geometries[0];
100103
const annotation = scatter.chart.annotation();
101104
// @ts-ignore
@@ -106,5 +109,50 @@ describe('scatter', () => {
106109
// @ts-ignore
107110
expect(elements[0].getModel().style.fill).toBe('#5B8FF9');
108111
expect(elements[0].getModel().size).toBe(5);
112+
const { width } = scatter.chart;
113+
const pathGroup = scatter.chart
114+
.getComponents()
115+
.find((item) => item.type === 'annotation')
116+
.component.cfg.group.cfg.children[0].getChildren();
117+
const { path } = pathGroup?.[0]?.cfg?.attrs;
118+
expect(path.length).toBe(3);
119+
expect(scatter.chart.getXScale().scale(8) * width < path[0][1]).toBeTruthy();
120+
});
121+
it('regressionLine: algorithm callback', async () => {
122+
const scatter = new Scatter(createDiv('regressionLine*algorithm'), {
123+
data,
124+
width: 400,
125+
height: 300,
126+
appendPadding: 10,
127+
xField: 'x',
128+
yField: 'y',
129+
size: 5,
130+
pointStyle: {
131+
stroke: '#777777',
132+
lineWidth: 1,
133+
fill: '#5B8FF9',
134+
},
135+
animation: false,
136+
regressionLine: {
137+
algorithm: (data) => {
138+
return [
139+
[8, 6],
140+
[16, 7],
141+
[24, 7],
142+
];
143+
},
144+
},
145+
});
146+
147+
scatter.render();
148+
await delay(500);
149+
const { width } = scatter.chart;
150+
const pathGroup = scatter.chart
151+
.getComponents()
152+
.find((item) => item.type === 'annotation')
153+
.component.cfg.group.cfg.children[0].getChildren();
154+
const { path } = pathGroup?.[0]?.cfg?.attrs;
155+
expect(path.length).toBe(3);
156+
expect(scatter.chart.getXScale().scale(8) * width < path[0][1]).toBeTruthy();
109157
});
110158
});

__tests__/unit/utils/path-spec.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { getSplinePath } from '../../../src/utils/path';
2+
3+
describe('PathUtil', () => {
4+
test('getSplinePath(), two points', () => {
5+
const points = [
6+
{ x: 0, y: 0 },
7+
{ x: 0.1, y: 0.1 },
8+
];
9+
const path = getSplinePath(points);
10+
expect(path).toEqual([
11+
['M', 0, 0],
12+
['L', 0.1, 0.1],
13+
]);
14+
});
15+
16+
test('getSplinePath(), two points isInCircle', () => {
17+
const points = [
18+
{ x: 0, y: 0 },
19+
{ x: 0.1, y: 0.1 },
20+
];
21+
const path = getSplinePath(points, true);
22+
expect(path).toEqual([['M', 0, 0], ['L', 0.1, 0.1], ['Z']]);
23+
});
24+
25+
test('getSplinePath(), more than two points', () => {
26+
const points = [
27+
{ x: 0, y: 0 },
28+
{ x: 0.1, y: 0.5 },
29+
{ x: 0.2, y: 0.1 },
30+
];
31+
const path = getSplinePath(points);
32+
expect(path.length).toBe(3);
33+
});
34+
35+
test('getSplinePath(), more than two points isInCircle', () => {
36+
const points = [
37+
{ x: 0, y: 0 },
38+
{ x: 0.1, y: 0.1 },
39+
{ x: 0.1, y: 0.3 },
40+
];
41+
const path = getSplinePath(points, true);
42+
expect(path.length).toBe(4);
43+
});
44+
});

src/plots/dual-axes/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ export class DualAxes extends Plot<DualAxesOptions> {
2020
geometryOptions,
2121
({ geometry }) => geometry === DualAxesGeometry.Line || geometry === undefined
2222
);
23-
2423
return deepAssign({}, super.getDefaultOptions(options), {
2524
yAxis: [],
26-
geometryOptions: [],
25+
geometryOptions,
2726
meta: {
2827
[xField]: {
2928
// x 轴一定是同步 scale 的

src/plots/scatter/adaptor.ts

+41-7
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { flow, deepAssign } from '../../utils';
33
import { point } from '../../adaptor/geometries';
44
import { tooltip, interaction, animation, theme, scale, annotation } from '../../adaptor/common';
55
import { findGeometry, transformLabel } from '../../utils';
6-
import { regressionLine } from './annotations/path';
7-
import { getQuadrantDefaultConfig } from './util';
6+
import { getQuadrantDefaultConfig, getPath } from './util';
87
import { ScatterOptions } from './types';
98

109
/**
@@ -83,15 +82,14 @@ function legend(params: Params<ScatterOptions>): Params<ScatterOptions> {
8382

8483
if (legend) {
8584
chart.legend(colorField || shapeField, legend);
85+
// 隐藏连续图例
86+
if (sizeField) {
87+
chart.legend(sizeField, false);
88+
}
8689
} else {
8790
chart.legend(false);
8891
}
8992

90-
// 隐藏连续图例
91-
if (sizeField) {
92-
chart.legend(sizeField, false);
93-
}
94-
9593
return params;
9694
}
9795

@@ -173,6 +171,42 @@ function scatterAnnotation(params: Params<ScatterOptions>): Params<ScatterOption
173171
return flow(annotation(annotationOptions))(params);
174172
}
175173

174+
// 趋势线
175+
function regressionLine(params: Params<ScatterOptions>): Params<ScatterOptions> {
176+
const { options, chart } = params;
177+
const { regressionLine } = options;
178+
if (regressionLine) {
179+
const { style } = regressionLine;
180+
const defaultStyle = {
181+
stroke: '#9ba29a',
182+
lineWidth: 2,
183+
opacity: 0.5,
184+
};
185+
chart.annotation().shape({
186+
render: (container, view) => {
187+
const group = container.addGroup({
188+
id: `${chart.id}-regression-line`,
189+
name: 'regression-line-group',
190+
});
191+
const path = getPath({
192+
view,
193+
options,
194+
});
195+
group.addShape('path', {
196+
name: 'regression-line',
197+
attrs: {
198+
path,
199+
...defaultStyle,
200+
...style,
201+
},
202+
});
203+
},
204+
});
205+
}
206+
207+
return params;
208+
}
209+
176210
/**
177211
* 散点图适配器
178212
* @param chart

0 commit comments

Comments
 (0)