Skip to content

Commit a823483

Browse files
authored
line chart: introduce renderer (#4249)
Renderer is an abstraction over how we are going to draw line, text, and rect. For now, we only support two renderer, SVG and THREE.js based WebGL. It is worth to note that both renderers we are supporting uses Object to represent a pixel--SVG uses elements to describe path, circle, and text and THREE.js uses Object3d to represent shader. Contrary to canvas renderer that renders pixels, for an update, the object based renderer allow us make modification without erasing entire scene/canvas/DOM to guarantee correctness. This allows us to make optimization where we minimally make changes to an object and let the library/browser make an efficient update to pixels. Based on this idea, our renderer in its shape rendering API takes an object as a parameter to update an object instead of creating one.
1 parent 7c48083 commit a823483

File tree

10 files changed

+709
-3
lines changed

10 files changed

+709
-3
lines changed

Diff for: tensorboard/webapp/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ tf_ng_web_test_suite(
283283
"//tensorboard/webapp/widgets/histogram:histogram_test",
284284
"//tensorboard/webapp/widgets/line_chart:line_chart_test",
285285
"//tensorboard/webapp/widgets/line_chart_v2/lib:lib_tests",
286+
"//tensorboard/webapp/widgets/line_chart_v2/lib/renderer:renderer_test",
286287
"//tensorboard/webapp/widgets/range_input:range_input_tests",
287288
"//tensorboard/webapp/widgets/text:text_tests",
288289
],

Diff for: tensorboard/webapp/widgets/line_chart_v2/lib/README.md

+11-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,19 @@ This is a generic charting library with a focus on performance.
66

77
- Agnostic to HTMLCanvas vs. OffscreenCanvas
88
- Generic to different implementation of renderer; SVG vs. WebGL.
9-
- Minimize cache eviction and try to do a minimal work for render
9+
- Do a minimal work; for example, it should only do a coordinate system conversion upon
10+
requested and it should do the minimal operation on rendered object.
1011

11-
### Jargons we define
12+
### Key concepts
1213

1314
- coordinatior: A utility module for converting coordinate systems. Abstracts
1415
away certain renderer quirks and holds onto state, helping with performance
1516
optimizations.
17+
- renderer: A pure module responsible for shape rendering (e.g., line, rect, triangle,
18+
etc...) of a given technology such as SVG or Three.js.
19+
- data drawable: a view that draws renders data in a reigon given. Examples of data
20+
drawable are series line and bar drawers. As implementer of a new visualizer should
21+
subclass DataDrawable and implement a `redraw` method. The base class maintains both
22+
data and render caches and let visualizer focus on the `redraw` method.
23+
- paint brush: primitives, such as `setLine`, for a data drawable that should be used
24+
inside a `redraw` method.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
load("//tensorboard/defs:defs.bzl", "tf_ts_library")
2+
3+
package(default_visibility = ["//tensorboard:internal"])
4+
5+
tf_ts_library(
6+
name = "renderer",
7+
srcs = [
8+
"index.ts",
9+
"svg_renderer.ts",
10+
"threejs_renderer.ts",
11+
],
12+
deps = [
13+
":types",
14+
"//tensorboard/webapp/third_party:d3",
15+
"//tensorboard/webapp/widgets/line_chart_v2/lib:coordinator",
16+
"//tensorboard/webapp/widgets/line_chart_v2/lib:types",
17+
"//tensorboard/webapp/widgets/line_chart_v2/lib:utils",
18+
"@npm//three",
19+
],
20+
)
21+
22+
tf_ts_library(
23+
name = "types",
24+
srcs = [
25+
"renderer_types.ts",
26+
],
27+
deps = [
28+
"//tensorboard/webapp/widgets/line_chart_v2/lib:types",
29+
],
30+
)
31+
32+
tf_ts_library(
33+
name = "renderer_test",
34+
testonly = True,
35+
srcs = [
36+
"renderer_test.ts",
37+
],
38+
deps = [
39+
":renderer",
40+
":types",
41+
"//tensorboard/webapp/widgets/line_chart_v2/lib:coordinator",
42+
"//tensorboard/webapp/widgets/line_chart_v2/lib:types",
43+
"@npm//@types/jasmine",
44+
"@npm//three",
45+
],
46+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* Copyright 2020 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
16+
export * from './renderer_types';
17+
export {SvgRenderer} from './svg_renderer';
18+
export {ThreeRenderer} from './threejs_renderer';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/* Copyright 2020 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import * as THREE from 'three';
16+
17+
import {ThreeCoordinator} from '../threejs_coordinator';
18+
import {Polyline} from '../types';
19+
import {SvgRenderer} from './svg_renderer';
20+
import {ThreeRenderer} from './threejs_renderer';
21+
22+
describe('line_chart_v2/lib/renderer test', () => {
23+
const SVG_NS = 'http://www.w3.org/2000/svg';
24+
const DEFAULT_LINE_OPTIONS = {visible: true, color: '#f00', width: 6};
25+
26+
describe('svg renderer', () => {
27+
let renderer: SvgRenderer;
28+
let el: SVGElement;
29+
30+
beforeEach(() => {
31+
el = document.createElementNS(SVG_NS, 'svg');
32+
renderer = new SvgRenderer(el);
33+
});
34+
35+
it('creates a line', () => {
36+
expect(el.children.length).toBe(0);
37+
38+
renderer.createOrUpdateLineObject(
39+
null,
40+
new Float32Array([0, 10, 10, 100]),
41+
{visible: true, color: '#f00', width: 6}
42+
);
43+
44+
expect(el.children.length).toBe(1);
45+
const path = el.children[0] as SVGPathElement;
46+
expect(path.tagName).toBe('path');
47+
expect(path.getAttribute('d')).toBe('M0,10L10,100');
48+
expect(path.style.stroke).toBe('rgb(255, 0, 0)');
49+
expect(path.style.strokeWidth).toBe('6');
50+
expect(path.style.display).toBe('');
51+
});
52+
53+
it('updates a cached path and styles', () => {
54+
const cacheObject = renderer.createOrUpdateLineObject(
55+
null,
56+
new Float32Array([0, 10, 10, 100]),
57+
{visible: true, color: '#f00', width: 6}
58+
);
59+
60+
renderer.createOrUpdateLineObject(
61+
cacheObject,
62+
new Float32Array([0, 5, 5, 50]),
63+
{visible: true, color: '#0f0', width: 3}
64+
);
65+
66+
expect(el.children.length).toBe(1);
67+
const path = el.children[0] as SVGPathElement;
68+
expect(path.tagName).toBe('path');
69+
expect(path.getAttribute('d')).toBe('M0,5L5,50');
70+
expect(path.style.stroke).toBe('rgb(0, 255, 0)');
71+
expect(path.style.strokeWidth).toBe('3');
72+
expect(path.style.display).toBe('');
73+
});
74+
75+
it('updates a cached path to an empty polyline', () => {
76+
const cacheObject = renderer.createOrUpdateLineObject(
77+
null,
78+
new Float32Array([0, 10, 10, 100]),
79+
{visible: true, color: '#f00', width: 6}
80+
);
81+
82+
renderer.createOrUpdateLineObject(cacheObject, new Float32Array(0), {
83+
visible: true,
84+
color: '#0f0',
85+
width: 3,
86+
});
87+
88+
expect(el.children.length).toBe(1);
89+
const path = el.children[0] as SVGPathElement;
90+
expect(path.tagName).toBe('path');
91+
expect(path.getAttribute('d')).toBe('');
92+
// While it is possible to update minimally and only change the path or visibility,
93+
// such optimization is a bit too premature without a clear benefit.
94+
expect(path.style.stroke).toBe('rgb(0, 255, 0)');
95+
expect(path.style.strokeWidth).toBe('3');
96+
expect(path.style.display).toBe('');
97+
});
98+
99+
it('skips updating path and color if visibility goes from true to false', () => {
100+
const cacheObject = renderer.createOrUpdateLineObject(
101+
null,
102+
new Float32Array([0, 10, 10, 100]),
103+
{visible: true, color: '#f00', width: 6}
104+
);
105+
106+
renderer.createOrUpdateLineObject(
107+
cacheObject,
108+
new Float32Array([0, 5, 5, 50]),
109+
{visible: false, color: '#0f0', width: 3}
110+
);
111+
112+
expect(el.children.length).toBe(1);
113+
const path = el.children[0] as SVGPathElement;
114+
expect(path.tagName).toBe('path');
115+
expect(path.style.display).toBe('none');
116+
expect(path.getAttribute('d')).toBe('M0,10L10,100');
117+
expect(path.style.stroke).toBe('rgb(255, 0, 0)');
118+
expect(path.style.strokeWidth).toBe('6');
119+
});
120+
121+
it('skips rendering DOM when a new cacheId starts with visible=false', () => {
122+
renderer.createOrUpdateLineObject(
123+
null,
124+
new Float32Array([0, 10, 10, 100]),
125+
{visible: false, color: '#f00', width: 6}
126+
);
127+
128+
expect(el.children.length).toBe(0);
129+
});
130+
});
131+
132+
describe('threejs renderer', () => {
133+
let renderer: ThreeRenderer;
134+
let scene: THREE.Scene;
135+
136+
function assertLine(line: THREE.Line, polyline: Polyline) {
137+
const geometry = line.geometry as THREE.BufferGeometry;
138+
const positions = geometry.getAttribute(
139+
'position'
140+
) as THREE.BufferAttribute;
141+
let positionIndex = 0;
142+
for (
143+
let polylineIndex = 0;
144+
polylineIndex < polyline.length;
145+
polylineIndex += 2
146+
) {
147+
const expectedX = polyline[polylineIndex];
148+
const expectedY = polyline[polylineIndex + 1];
149+
const actualX = positions.array[positionIndex++];
150+
const actualY = positions.array[positionIndex++];
151+
const actualZ = positions.array[positionIndex++];
152+
expect(actualX).toBe(expectedX);
153+
expect(actualY).toBe(expectedY);
154+
expect(actualZ).toBe(0);
155+
}
156+
}
157+
158+
function assertMaterial(
159+
line: THREE.Line,
160+
longHexString: string,
161+
visibility: boolean
162+
) {
163+
const material = line.material as THREE.LineBasicMaterial;
164+
expect(material.visible).toBe(visibility);
165+
expect(material.color.getHexString()).toBe(longHexString.slice(1));
166+
}
167+
168+
beforeEach(() => {
169+
scene = new THREE.Scene();
170+
spyOn(THREE, 'Scene').and.returnValue(scene);
171+
172+
const canvas = document.createElement('canvas');
173+
const coordinator = new ThreeCoordinator();
174+
renderer = new ThreeRenderer(canvas, coordinator, 2);
175+
});
176+
177+
it('creates a line', () => {
178+
renderer.createOrUpdateLineObject(
179+
null,
180+
new Float32Array([0, 10, 10, 100]),
181+
{visible: true, color: '#f00', width: 6}
182+
);
183+
184+
expect(scene.children.length).toBe(1);
185+
const lineObject = scene.children[0] as THREE.Line;
186+
expect(lineObject).toBeInstanceOf(THREE.Line);
187+
assertLine(lineObject, new Float32Array([0, 10, 10, 100]));
188+
assertMaterial(lineObject, '#ff0000', true);
189+
});
190+
191+
it('updates cached path and styles', () => {
192+
const cacheObject = renderer.createOrUpdateLineObject(
193+
null,
194+
new Float32Array([0, 10, 10, 100]),
195+
{visible: true, color: '#f00', width: 6}
196+
);
197+
198+
renderer.createOrUpdateLineObject(
199+
cacheObject,
200+
new Float32Array([0, 5, 5, 50, 10, 100]),
201+
{visible: true, color: '#0f0', width: 3}
202+
);
203+
204+
const lineObject = scene.children[0] as THREE.Line;
205+
assertLine(lineObject, new Float32Array([0, 5, 5, 50, 10, 100]));
206+
assertMaterial(lineObject, '#00ff00', true);
207+
});
208+
209+
it('updates object when going from non-emtpy polyline to an empty one', () => {
210+
const cacheObject = renderer.createOrUpdateLineObject(
211+
null,
212+
new Float32Array([0, 10, 10, 100]),
213+
{visible: true, color: '#f00', width: 6}
214+
);
215+
216+
renderer.createOrUpdateLineObject(cacheObject, new Float32Array(0), {
217+
visible: true,
218+
color: '#0f0',
219+
width: 3,
220+
});
221+
222+
const lineObject = scene.children[0] as THREE.Line;
223+
assertLine(lineObject, new Float32Array(0));
224+
assertMaterial(lineObject, '#00ff00', true);
225+
});
226+
227+
it('does not update color and paths when visibility go from true to false', () => {
228+
const cachedObject = renderer.createOrUpdateLineObject(
229+
null,
230+
new Float32Array([0, 10, 10, 100]),
231+
{visible: true, color: '#f00', width: 6}
232+
);
233+
234+
renderer.createOrUpdateLineObject(
235+
cachedObject,
236+
new Float32Array([0, 5, 5, 50, 10, 100]),
237+
{visible: false, color: '#0f0', width: 3}
238+
);
239+
240+
const lineObject = scene.children[0] as THREE.Line;
241+
assertLine(lineObject, new Float32Array([0, 10, 10, 100]));
242+
assertMaterial(lineObject, '#ff0000', false);
243+
});
244+
245+
it('skips rendering if render starts with visibility=false ', () => {
246+
renderer.createOrUpdateLineObject(null, new Float32Array([0, 1, 0, 1]), {
247+
...DEFAULT_LINE_OPTIONS,
248+
visible: false,
249+
});
250+
251+
expect(scene.children.length).toBe(0);
252+
});
253+
});
254+
});

0 commit comments

Comments
 (0)