@pie-lib/plot 2.27.2 → 2.27.3-next.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,13 @@
1
- import { shallow } from 'enzyme';
1
+ import { render, cleanup } from '@testing-library/react';
2
2
  import React from 'react';
3
3
  import { Root } from '../root';
4
4
  import { select, mouse } from 'd3-selection';
5
5
 
6
+ jest.mock('d3-selection', () => ({
7
+ select: jest.fn(),
8
+ mouse: jest.fn(),
9
+ }));
10
+
6
11
  const scaleMock = () => {
7
12
  const fn = jest.fn((n) => n);
8
13
  fn.invert = jest.fn((n) => n);
@@ -34,83 +39,234 @@ const graphProps = () => ({
34
39
  },
35
40
  });
36
41
 
37
- const wrapper = (props) => {
38
- props = {
39
- classes: {},
40
- graphProps: graphProps(),
41
- ...props,
42
- };
42
+ describe('root', () => {
43
+ let mockOn;
44
+ let defaultProps;
43
45
 
44
- return shallow(<Root {...props}>hi</Root>, { disableLifecycleMethods: true });
45
- };
46
+ beforeEach(() => {
47
+ mockOn = jest.fn();
48
+ select.mockReturnValue({
49
+ on: mockOn,
50
+ });
51
+ mouse.mockReturnValue([0, 0]);
46
52
 
47
- jest.mock('d3-selection', () => ({
48
- select: jest.fn(),
49
- mouse: jest.fn(),
50
- }));
53
+ defaultProps = {
54
+ classes: {},
55
+ graphProps: graphProps(),
56
+ };
57
+ });
51
58
 
52
- describe('root', () => {
53
- describe('snapshot', () => {
54
- it('matches', () => {
55
- const w = wrapper();
56
- expect(w).toMatchSnapshot();
57
- });
59
+ afterEach(() => {
60
+ cleanup();
61
+ jest.clearAllMocks();
62
+ });
63
+
64
+ it('renders with children', () => {
65
+ const { container, getByText } = render(
66
+ <Root {...defaultProps}>hi</Root>
67
+ );
68
+ expect(container.firstChild).toBeInTheDocument();
69
+ expect(getByText('hi')).toBeInTheDocument();
58
70
  });
59
71
 
60
72
  describe('logic', () => {
61
73
  describe('mousemove', () => {
62
74
  describe('mount/unmount', () => {
63
- it('adds mousemove listener on compenentDidMount', () => {
64
- const w = wrapper();
65
- const g = {
66
- on: jest.fn(),
67
- };
68
- select.mockReturnValue(g);
69
- w.instance().componentDidMount();
70
- expect(g.on).toHaveBeenCalledWith('mousemove', expect.any(Function));
75
+ it('adds mousemove listener on componentDidMount', () => {
76
+ render(<Root {...defaultProps}>hi</Root>);
77
+
78
+ // Verify that select was called with the g element
79
+ expect(select).toHaveBeenCalled();
80
+
81
+ // Verify that on() was called with 'mousemove' and a function
82
+ expect(mockOn).toHaveBeenCalledWith('mousemove', expect.any(Function));
71
83
  });
72
- it('unsets mousemove listener on componentWillUnmount', () => {
73
- const w = wrapper();
74
- const g = {
75
- on: jest.fn(),
76
- };
77
- select.mockReturnValue(g);
78
- w.instance().componentWillUnmount();
79
- expect(g.on).toHaveBeenCalledWith('mousemove', null);
84
+
85
+ it('removes mousemove listener on componentWillUnmount', () => {
86
+ const { unmount } = render(<Root {...defaultProps}>hi</Root>);
87
+
88
+ // Clear previous calls to isolate unmount behavior
89
+ mockOn.mockClear();
90
+ select.mockClear();
91
+
92
+ unmount();
93
+
94
+ // Verify that select was called during unmount
95
+ expect(select).toHaveBeenCalled();
96
+
97
+ // Verify that on() was called with 'mousemove' and null to remove the listener
98
+ expect(mockOn).toHaveBeenCalledWith('mousemove', null);
80
99
  });
81
100
  });
82
101
 
83
102
  describe('mouseMove function', () => {
84
- let onMouseMove, w, gp;
85
- beforeEach(() => {
86
- onMouseMove = jest.fn();
87
- gp = graphProps();
88
- w = wrapper({
103
+ it('calls mouse with correct arguments', () => {
104
+ const onMouseMove = jest.fn();
105
+ const gp = graphProps();
106
+ const props = {
107
+ ...defaultProps,
89
108
  onMouseMove,
90
109
  graphProps: gp,
110
+ };
111
+
112
+ const mockNode = document.createElement('div');
113
+ const mockSelection = {
114
+ _groups: [[mockNode]],
115
+ node: () => mockNode,
116
+ };
117
+
118
+ // Mock select to return our mockSelection
119
+ select.mockReturnValue({
120
+ ...mockSelection,
121
+ on: (event, handler) => {
122
+ mockOn(event, handler);
123
+ // When 'mousemove' is registered, immediately test it
124
+ if (event === 'mousemove' && handler) {
125
+ mouse.mockReturnValue([10, 20]);
126
+ // Handler is bound with mockSelection as first arg, so call with no args
127
+ handler();
128
+ }
129
+ },
91
130
  });
92
- mouse.mockReturnValue([0, 0]);
93
- const g = { _groups: [[[0, 0]]] };
94
- w.instance().mouseMove(g);
95
- });
96
- it('calls mouse', () => {
97
- expect(mouse).toHaveBeenCalledWith([0, 0]);
98
- });
99
- it('calls, scale.x.invert', () => {
100
- expect(gp.scale.x.invert).toHaveBeenCalledWith(0);
131
+
132
+ render(<Root {...props}>hi</Root>);
133
+
134
+ // Verify mouse was called with the correct node
135
+ expect(mouse).toHaveBeenCalledWith(mockNode);
101
136
  });
102
- it('calls, scale.y.invert', () => {
103
- expect(gp.scale.y.invert).toHaveBeenCalledWith(0);
137
+
138
+ it('calls scale.x.invert and scale.y.invert', () => {
139
+ const onMouseMove = jest.fn();
140
+ const gp = graphProps();
141
+ const props = {
142
+ ...defaultProps,
143
+ onMouseMove,
144
+ graphProps: gp,
145
+ };
146
+
147
+ const mockNode = document.createElement('div');
148
+ const mockSelection = {
149
+ _groups: [[mockNode]],
150
+ node: () => mockNode,
151
+ };
152
+
153
+ select.mockReturnValue({
154
+ ...mockSelection,
155
+ on: (event, handler) => {
156
+ mockOn(event, handler);
157
+ if (event === 'mousemove' && handler) {
158
+ mouse.mockReturnValue([15, 25]);
159
+ handler();
160
+ }
161
+ },
162
+ });
163
+
164
+ render(<Root {...props}>hi</Root>);
165
+
166
+ expect(gp.scale.x.invert).toHaveBeenCalledWith(15);
167
+ expect(gp.scale.y.invert).toHaveBeenCalledWith(25);
104
168
  });
105
- it('calls, snap.x', () => {
106
- expect(gp.snap.x).toHaveBeenCalledWith(0);
169
+
170
+ it('calls snap.x and snap.y with inverted coordinates', () => {
171
+ const onMouseMove = jest.fn();
172
+ const gp = graphProps();
173
+ gp.scale.x.invert = jest.fn().mockReturnValue(5);
174
+ gp.scale.y.invert = jest.fn().mockReturnValue(10);
175
+ const props = {
176
+ ...defaultProps,
177
+ onMouseMove,
178
+ graphProps: gp,
179
+ };
180
+
181
+ const mockNode = document.createElement('div');
182
+ const mockSelection = {
183
+ _groups: [[mockNode]],
184
+ node: () => mockNode,
185
+ };
186
+
187
+ select.mockReturnValue({
188
+ ...mockSelection,
189
+ on: (event, handler) => {
190
+ mockOn(event, handler);
191
+ if (event === 'mousemove' && handler) {
192
+ mouse.mockReturnValue([15, 25]);
193
+ handler();
194
+ }
195
+ },
196
+ });
197
+
198
+ render(<Root {...props}>hi</Root>);
199
+
200
+ expect(gp.snap.x).toHaveBeenCalledWith(5);
201
+ expect(gp.snap.y).toHaveBeenCalledWith(10);
107
202
  });
108
- it('calls, snap.y', () => {
109
- expect(gp.snap.y).toHaveBeenCalledWith(0);
203
+
204
+ it('calls onMouseMove handler with snapped coordinates', () => {
205
+ const onMouseMove = jest.fn();
206
+ const gp = graphProps();
207
+ gp.scale.x.invert = jest.fn().mockReturnValue(7);
208
+ gp.scale.y.invert = jest.fn().mockReturnValue(14);
209
+ gp.snap.x = jest.fn().mockReturnValue(10);
210
+ gp.snap.y = jest.fn().mockReturnValue(15);
211
+
212
+ const props = {
213
+ ...defaultProps,
214
+ onMouseMove,
215
+ graphProps: gp,
216
+ };
217
+
218
+ const mockNode = document.createElement('div');
219
+ const mockSelection = {
220
+ _groups: [[mockNode]],
221
+ node: () => mockNode,
222
+ };
223
+
224
+ select.mockReturnValue({
225
+ ...mockSelection,
226
+ on: (event, handler) => {
227
+ mockOn(event, handler);
228
+ if (event === 'mousemove' && handler) {
229
+ mouse.mockReturnValue([100, 200]);
230
+ handler();
231
+ }
232
+ },
233
+ });
234
+
235
+ render(<Root {...props}>hi</Root>);
236
+
237
+ expect(onMouseMove).toHaveBeenCalledWith({ x: 10, y: 15 });
110
238
  });
111
239
 
112
- it('calls handler', () => {
113
- expect(onMouseMove).toHaveBeenCalledWith({ x: 0, y: 0 });
240
+ it('does not call onMouseMove when handler is not provided', () => {
241
+ const gp = graphProps();
242
+ const props = {
243
+ ...defaultProps,
244
+ graphProps: gp,
245
+ };
246
+
247
+ const mockNode = document.createElement('div');
248
+ const mockSelection = {
249
+ _groups: [[mockNode]],
250
+ node: () => mockNode,
251
+ };
252
+
253
+ select.mockReturnValue({
254
+ ...mockSelection,
255
+ on: (event, handler) => {
256
+ mockOn(event, handler);
257
+ if (event === 'mousemove' && handler) {
258
+ mouse.mockReturnValue([100, 200]);
259
+ // Should not throw error when onMouseMove is not provided
260
+ expect(() => handler()).not.toThrow();
261
+ }
262
+ },
263
+ });
264
+
265
+ render(<Root {...props}>hi</Root>);
266
+
267
+ // Verify scale methods were not called (early return in mouseMove)
268
+ expect(gp.scale.x.invert).not.toHaveBeenCalled();
269
+ expect(gp.scale.y.invert).not.toHaveBeenCalled();
114
270
  });
115
271
  });
116
272
  });
@@ -38,6 +38,15 @@ export const gridDraggable = (opts) => (Comp) => {
38
38
  onMove: PropTypes.func,
39
39
  graphProps: GraphPropsType.isRequired,
40
40
  };
41
+
42
+ constructor(props) {
43
+ super(props);
44
+ this.state = {
45
+ startX: null,
46
+ startY: null,
47
+ };
48
+ }
49
+
41
50
  grid = () => {
42
51
  const { graphProps } = this.props;
43
52
  const { scale, domain, range } = graphProps;
@@ -87,10 +96,10 @@ export const gridDraggable = (opts) => (Comp) => {
87
96
  const grid = this.grid();
88
97
 
89
98
  const scaled = {
90
- left: (bounds.left / grid.interval) * grid.x,
91
- right: (bounds.right / grid.interval) * grid.x,
92
- top: (bounds.top / grid.interval) * grid.y,
93
- bottom: (bounds.bottom / grid.interval) * grid.y,
99
+ left: bounds.left * grid.x,
100
+ right: bounds.right * grid.x,
101
+ top: bounds.top * grid.y,
102
+ bottom: bounds.bottom * grid.y,
94
103
  };
95
104
  log('[getScaledBounds]: ', scaled);
96
105
  return scaled;
package/src/label.jsx CHANGED
@@ -1,13 +1,53 @@
1
- import React, { useState } from 'react';
2
- import { color, Readable } from '@pie-lib/render-ui';
3
- import cn from 'classnames';
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Readable } from '@pie-lib/render-ui';
4
3
  import EditableHtml from '@pie-lib/editable-html';
5
- import { withStyles } from '@material-ui/core/styles';
6
4
  import PropTypes from 'prop-types';
7
5
  import { extractTextFromHTML, isEmptyString } from './utils';
6
+
7
+ const styles = {
8
+ axisLabel: {
9
+ fontSize: 12,
10
+ textAlign: 'center',
11
+ margin: 4,
12
+ padding: '4px 0',
13
+ },
14
+ chartLabel: {
15
+ fontSize: 16,
16
+ textAlign: 'center',
17
+ margin: 4,
18
+ padding: '4px 0',
19
+ },
20
+ disabledLabel: {
21
+ pointerEvents: 'none',
22
+ width: '100%',
23
+ },
24
+ editLabel: {
25
+ position: 'absolute',
26
+ backgroundColor: 'white',
27
+ borderRadius: 4,
28
+ boxShadow: '0px 5px 8px rgba(0,0,0,0.15)',
29
+ zIndex: 10,
30
+ },
31
+ rotateLeftLabel: {
32
+ transform: 'rotate(-90deg)',
33
+ transformOrigin: '0 0',
34
+ position: 'absolute',
35
+ },
36
+ rotateRightLabel: {
37
+ transform: 'rotate(90deg)',
38
+ transformOrigin: '0 0',
39
+ position: 'absolute',
40
+ },
41
+ customBottom: {
42
+ position: 'absolute',
43
+ },
44
+ displayNone: {
45
+ display: 'none',
46
+ },
47
+ };
48
+
8
49
  const LabelComponent = (props) => {
9
50
  const {
10
- classes,
11
51
  disabledLabel,
12
52
  graphHeight,
13
53
  graphWidth,
@@ -23,21 +63,32 @@ const LabelComponent = (props) => {
23
63
  charactersLimit,
24
64
  titleHeight,
25
65
  } = props;
26
- const [rotatedToHorizontal, setRotatedToHorizontal] = useState(false);
66
+
67
+ const [rotatedToHorizontal, setRotatedToHorizontal] = useState(false);
68
+
27
69
  const activePlugins = [
28
70
  'bold',
29
71
  'italic',
30
72
  'underline',
31
73
  'strikethrough',
32
74
  'math',
33
- // 'languageCharacters'
34
75
  ];
35
76
 
36
- const isChart = isChartBottomLabel || isChartLeftLabel || isDefineChartBottomLabel || isDefineChartLeftLabel;
77
+ const isChart =
78
+ isChartBottomLabel ||
79
+ isChartLeftLabel ||
80
+ isDefineChartBottomLabel ||
81
+ isDefineChartLeftLabel;
82
+
83
+ const chartValue =
84
+ side === 'left' && isDefineChartLeftLabel && graphHeight - 220;
37
85
 
38
- const chartValue = side === 'left' && isDefineChartLeftLabel && graphHeight - 220;
39
86
  const defaultStyle = {
40
- width: chartValue || (side === 'left' || side === 'right' ? graphHeight - 8 : graphWidth - 8),
87
+ width:
88
+ chartValue ||
89
+ (side === 'left' || side === 'right'
90
+ ? graphHeight - 8
91
+ : graphWidth - 8),
41
92
  top:
42
93
  chartValue ||
43
94
  (isChartLeftLabel && `${graphHeight - 70}px`) ||
@@ -54,27 +105,53 @@ const LabelComponent = (props) => {
54
105
 
55
106
  const rotatedStyle = {
56
107
  width: graphWidth - 8,
57
- top: (side === 'right' && `${graphHeight - 22}px`) || 0,
108
+ top: side === 'right' ? `${graphHeight - 22}px` : 0,
58
109
  left: 0,
59
110
  };
60
111
 
61
- const rotateLabel = () => !disabledLabel && (side === 'left' || side === 'right') && setRotatedToHorizontal(true);
112
+ const rotateLabel = () => {
113
+ if (!disabledLabel && (side === 'left' || side === 'right')) {
114
+ setRotatedToHorizontal(true);
115
+ }
116
+ };
117
+
118
+ const exitEditMode = () => {
119
+ setRotatedToHorizontal(false);
120
+
121
+ // blur active element because rotation is causing editing issues on exit
122
+ requestAnimationFrame(() => {
123
+ document.activeElement?.blur?.();
124
+ });
125
+ };
62
126
 
63
127
  return (
64
128
  <Readable false>
65
129
  <div
66
- className={cn(isChart ? classes.chartLabel : classes.axisLabel, {
67
- [classes.rotateLeftLabel]: side === 'left' && !rotatedToHorizontal,
68
- [classes.rotateRightLabel]: side === 'right' && !rotatedToHorizontal,
69
- [classes.editLabel]: rotatedToHorizontal,
70
- [classes.customBottom]: isChartBottomLabel || isDefineChartBottomLabel,
71
- [classes.displayNone]: disabledLabel && !isChart && isEmptyString(extractTextFromHTML(text)),
72
- })}
73
- style={rotatedToHorizontal ? rotatedStyle : defaultStyle}
74
130
  onClick={rotateLabel}
131
+ style={{
132
+ ...(rotatedToHorizontal ? rotatedStyle : defaultStyle),
133
+ ...(isChart ? styles.chartLabel : styles.axisLabel),
134
+ ...(side === 'left' && !rotatedToHorizontal
135
+ ? styles.rotateLeftLabel
136
+ : {}),
137
+ ...(side === 'right' && !rotatedToHorizontal
138
+ ? styles.rotateRightLabel
139
+ : {}),
140
+ ...(rotatedToHorizontal ? styles.editLabel : {}),
141
+ ...((isChartBottomLabel || isDefineChartBottomLabel)
142
+ ? styles.customBottom
143
+ : {}),
144
+ ...((disabledLabel &&
145
+ !isChart &&
146
+ isEmptyString(extractTextFromHTML(text))) &&
147
+ styles.displayNone),
148
+ }}
75
149
  >
76
150
  {disabledLabel ? (
77
- <div className={classes.disabledLabel} dangerouslySetInnerHTML={{ __html: text || '' }} />
151
+ <div
152
+ style={styles.disabledLabel}
153
+ dangerouslySetInnerHTML={{ __html: text || '' }}
154
+ />
78
155
  ) : (
79
156
  <EditableHtml
80
157
  markup={text || ''}
@@ -87,7 +164,7 @@ const LabelComponent = (props) => {
87
164
  }}
88
165
  disableScrollbar
89
166
  activePlugins={activePlugins}
90
- onDone={() => setRotatedToHorizontal(false)}
167
+ onDone={exitEditMode}
91
168
  mathMlOptions={mathMlOptions}
92
169
  charactersLimit={charactersLimit}
93
170
  />
@@ -96,8 +173,8 @@ const LabelComponent = (props) => {
96
173
  </Readable>
97
174
  );
98
175
  };
176
+
99
177
  LabelComponent.propTypes = {
100
- classes: PropTypes.object,
101
178
  disabledLabel: PropTypes.bool,
102
179
  graphHeight: PropTypes.number,
103
180
  graphWidth: PropTypes.number,
@@ -114,49 +191,4 @@ LabelComponent.propTypes = {
114
191
  titleHeight: PropTypes.number,
115
192
  };
116
193
 
117
- export default withStyles((theme) => ({
118
- label: {
119
- fill: color.secondary(),
120
- },
121
- axisLabel: {
122
- fontSize: theme.typography.fontSize - 2,
123
- textAlign: 'center',
124
- margin: theme.spacing.unit / 2,
125
- padding: `${theme.spacing.unit / 2}px 0`,
126
- },
127
- chartLabel: {
128
- fontSize: theme.typography.fontSize + 2,
129
- textAlign: 'center',
130
- margin: theme.spacing.unit / 2,
131
- padding: `${theme.spacing.unit / 2}px 0`,
132
- },
133
- disabledLabel: {
134
- pointerEvents: 'none',
135
- width: '100%',
136
- },
137
- editLabel: {
138
- position: 'absolute',
139
- backgroundColor: 'white',
140
- borderRadius: '4px',
141
- boxShadow: '0px 5px 8px rgba(0, 0, 0, 0.15)',
142
- zIndex: 10,
143
- },
144
- rotateLeftLabel: {
145
- '-webkit-transform': 'rotate(-90deg)',
146
- transformOrigin: '0 0',
147
- transformStyle: 'preserve-3d',
148
- position: 'absolute',
149
- },
150
- rotateRightLabel: {
151
- '-webkit-transform': 'rotate(90deg)',
152
- transformOrigin: '0 0',
153
- transformStyle: 'preserve-3d',
154
- position: 'absolute',
155
- },
156
- customBottom: {
157
- position: 'absolute',
158
- },
159
- displayNone: {
160
- display: 'none',
161
- },
162
- }))(LabelComponent);
194
+ export default LabelComponent;