1- import { Key , MouseButton , throwIfNil , unionOfClientRects } from '@hypertrace/common' ;
2- import { brush , BrushBehavior , D3BrushEvent } from 'd3-brush' ;
3- // tslint:disable-next-line: no-restricted-globals weird tslint error. Rename event so we can type it and not mistake it for other events
4- import { event as _d3CurrentEvent , Selection } from 'd3-selection' ;
5- import { D3ZoomEvent , zoom , ZoomBehavior , zoomIdentity , ZoomTransform } from 'd3-zoom' ;
6- import { isEqual } from 'lodash-es' ;
7- import { BehaviorSubject , Observable } from 'rxjs' ;
1+ import { throwIfNil , unionOfClientRects } from '@hypertrace/common' ;
2+ import { D3Zoom } from '../../../../utils/d3/zoom/d3-zoom' ;
83import { RenderableTopologyNode , RenderableTopologyNodeRenderedData } from '../../../topology' ;
4+ import { D3ZoomConfiguration } from './../../../../utils/d3/zoom/d3-zoom' ;
95
10- export class TopologyZoom < TContainer extends Element = Element , TTarget extends Element = Element > {
11- private static readonly DEFAULT_MIN_ZOOM : number = 0.2 ;
12- private static readonly DEFAULT_MAX_ZOOM : number = 5.0 ;
13- private static readonly DATA_BRUSH_CONTEXT_CLASS : string = 'brush-context' ;
14- private static readonly DATA_BRUSH_OVERLAY_CLASS : string = 'overlay' ;
15- private static readonly DATA_BRUSH_SELECTION_CLASS : string = 'selection' ;
16-
17- private static readonly DATA_BRUSH_OVERLAY_WIDTH : number = 200 ;
18- private static readonly DATA_BRUSH_OVERLAY_HEIGHT : number = 200 ;
19- private config ?: TopologyZoomConfiguration < TContainer , TTarget > ;
20- private readonly zoomBehavior : ZoomBehavior < TContainer , unknown > ;
21- private readonly zoomChangeSubject : BehaviorSubject < number > = new BehaviorSubject ( 1 ) ;
22- private readonly brushBehaviour : BrushBehavior < unknown > ;
23- public readonly zoomChange$ : Observable < number > = this . zoomChangeSubject . asObservable ( ) ;
24-
25- private get minScale ( ) : number {
26- return this . config && this . config . minScale !== undefined ? this . config . minScale : TopologyZoom . DEFAULT_MIN_ZOOM ;
27- }
28-
29- private get maxScale ( ) : number {
30- return this . config && this . config . maxScale !== undefined ? this . config . maxScale : TopologyZoom . DEFAULT_MAX_ZOOM ;
31- }
32-
33- private getCurrentD3Event < T extends ZoomHandlerEvent | ZoomSourceEvent > ( ) : T {
34- // Returned event type depends on where this is invoked. Filters get a source event.
35- return _d3CurrentEvent ;
36- }
37-
38- public constructor ( ) {
39- this . zoomBehavior = zoom < TContainer , unknown > ( )
40- . filter ( ( ) => this . checkValidZoomEvent ( this . getCurrentD3Event ( ) ) )
41- . on ( 'zoom' , ( ) => this . updateZoom ( this . getCurrentD3Event < ZoomHandlerEvent > ( ) . transform ) )
42- . on ( 'start.drag' , ( ) => this . updateDraggingClassIfNeeded ( this . getCurrentD3Event ( ) ) )
43- . on ( 'end.drag' , ( ) => this . updateDraggingClassIfNeeded ( this . getCurrentD3Event ( ) ) ) ;
44-
45- this . brushBehaviour = brush < unknown > ( ) . on ( 'start end' , ( ) => this . onBrushSelection ( _d3CurrentEvent ) ) ;
46- }
47-
48- public attachZoom ( configuration : TopologyZoomConfiguration < TContainer , TTarget > ) : this {
49- this . config = configuration ;
50- this . zoomBehavior . scaleExtent ( [ this . minScale , this . maxScale ] ) ;
51- this . config . container
52- . call ( this . zoomBehavior )
53- // tslint:disable-next-line: no-null-keyword
54- . on ( 'dblclick.zoom' , null ) ; // Remove default double click handler
55-
56- return this ;
57- }
58-
59- public getZoomScale ( ) : number {
60- return this . zoomChangeSubject . getValue ( ) ;
61- }
62-
63- public setZoomScale ( factor : number ) : void {
64- this . zoomBehavior . scaleTo ( this . getContainerSelectionOrThrow ( ) , factor ) ;
65- }
66-
67- public resetZoom ( ) : void {
68- this . zoomBehavior . transform ( this . getContainerSelectionOrThrow ( ) , zoomIdentity ) ;
69- }
70-
71- public canIncreaseScale ( ) : boolean {
72- return this . maxScale > this . getZoomScale ( ) ;
73- }
74-
75- public canDecreaseScale ( ) : boolean {
76- return this . minScale < this . getZoomScale ( ) ;
77- }
78-
6+ export class TopologyZoom < TContainer extends Element = Element , TTarget extends Element = Element > extends D3Zoom <
7+ TContainer ,
8+ TTarget
9+ > {
7910 public zoomToFit ( nodes : RenderableTopologyNode [ ] ) : void {
8011 const nodeClientRects = nodes
8112 . map ( node => node . renderedData ( ) )
8213 . filter ( ( renderedData ) : renderedData is RenderableTopologyNodeRenderedData => ! ! renderedData )
8314 . map ( renderedData => renderedData . getBoudingBox ( ) ) ;
8415
8516 const requestedRect = unionOfClientRects ( ...nodeClientRects ) ;
86- const availableRect = throwIfNil ( this . config && this . config . container . node ( ) ) . getBoundingClientRect ( ) ;
87- // Add a bit of padding to requested width/height for padding
88- const requestedWidthScale = availableRect . width / ( requestedRect . width + 24 ) ;
89- const requestedHeightScale = availableRect . height / ( requestedRect . height + 24 ) ;
90- // Zoomed in more than this is fine, but this is min to fit everything
91- const minOverallScale = Math . min ( requestedWidthScale , requestedHeightScale ) ;
92- // Never zoom beyond 100% with zoom to fit
93- this . setZoomScale ( Math . min ( 1 , Math . max ( this . minScale , minOverallScale ) ) ) ;
94- this . translateToRect ( requestedRect ) ;
17+
18+ this . zoomToRect ( requestedRect ) ;
9519 }
9620
9721 public panToTopLeft ( nodes : RenderableTopologyNode [ ] ) : void {
@@ -104,17 +28,7 @@ export class TopologyZoom<TContainer extends Element = Element, TTarget extends
10428 this . panToRect ( unionOfClientRects ( ...nodeClientRects ) ) ;
10529 }
10630
107- public panToRect ( viewRect : ClientRect ) : void {
108- const availableRect = throwIfNil ( this . config && this . config . container . node ( ) ) . getBoundingClientRect ( ) ;
109- // AvailableRect is used for width since we are always keeping scale as 1
110- this . zoomBehavior . translateTo (
111- this . getContainerSelectionOrThrow ( ) ,
112- viewRect . left + availableRect . width / 2 ,
113- viewRect . top + availableRect . height / 2
114- ) ;
115- }
116-
117- public determineZoomScale ( nodes : RenderableTopologyNode [ ] , availableRect : ClientRect ) : number {
31+ private determineZoomScale ( nodes : RenderableTopologyNode [ ] , availableRect : ClientRect ) : number {
11832 const nodeClientRects = nodes
11933 . map ( node => node . renderedData ( ) )
12034 . filter ( ( renderedData ) : renderedData is RenderableTopologyNodeRenderedData => ! ! renderedData )
@@ -130,7 +44,7 @@ export class TopologyZoom<TContainer extends Element = Element, TTarget extends
13044 return minOverallScale ;
13145 }
13246
133- public updateBrushOverlay ( nodes : RenderableTopologyNode [ ] ) : void {
47+ public updateBrushOverlayWithData ( nodes : RenderableTopologyNode [ ] ) : void {
13448 const containerSelection = this . getContainerSelectionOrThrow ( ) ;
13549 containerSelection . select ( `.${ TopologyZoom . DATA_BRUSH_CONTEXT_CLASS } ` ) . remove ( ) ;
13650 const containerdBox = throwIfNil ( containerSelection . node ( ) ) . getBoundingClientRect ( ) ;
@@ -143,167 +57,12 @@ export class TopologyZoom<TContainer extends Element = Element, TTarget extends
14357 width : TopologyZoom . DATA_BRUSH_OVERLAY_WIDTH ,
14458 height : TopologyZoom . DATA_BRUSH_OVERLAY_HEIGHT
14559 } ;
146- const overlayZoomScale = this . determineZoomScale ( nodes , boundingBox ) ;
147- this . brushBehaviour . extent ( [
148- [ 0 , 0 ] ,
149- [
150- TopologyZoom . DATA_BRUSH_OVERLAY_WIDTH / overlayZoomScale ,
151- TopologyZoom . DATA_BRUSH_OVERLAY_HEIGHT / overlayZoomScale
152- ]
153- ] ) ;
154-
155- this . config ! . brushOverlay = throwIfNil ( this . config )
156- . target . clone ( true )
157- . classed ( TopologyZoom . DATA_BRUSH_CONTEXT_CLASS , true )
158- . attr ( 'width' , boundingBox . width )
159- . attr ( 'height' , boundingBox . height )
160- . attr ( 'transform' , `translate(${ boundingBox . left - 20 } , ${ boundingBox . top - 40 } ) scale(${ overlayZoomScale } )` )
161- . insert ( 'g' , '.entity-edge' )
162- // tslint:disable-next-line: no-any
163- . call ( this . brushBehaviour as any ) ;
164-
165- this . styleBrushSelection ( this . config ! . brushOverlay , overlayZoomScale ) ;
166- }
167-
168- private styleBrushSelection (
169- brushSelection : Selection < SVGGElement , unknown , null , undefined > ,
170- overlayZoomScale : number
171- ) : void {
172- // Map values
173- const overlayBorderRadius = 4 / overlayZoomScale ;
174- const selectionBorderRadius = 4 / overlayZoomScale ;
175- const strokeWidth = 1 / overlayZoomScale ;
176-
177- brushSelection
178- . select ( `.${ TopologyZoom . DATA_BRUSH_OVERLAY_CLASS } ` )
179- . attr ( 'rx' , overlayBorderRadius )
180- . attr ( 'ry' , overlayBorderRadius ) ;
181-
182- brushSelection
183- . select ( `.${ TopologyZoom . DATA_BRUSH_SELECTION_CLASS } ` )
184- . attr ( 'rx' , selectionBorderRadius )
185- . attr ( 'ry' , selectionBorderRadius )
186- . style ( 'stroke-width' , strokeWidth )
187- . style ( 'stroke-dasharray' , `${ strokeWidth } , ${ strokeWidth } ` ) ;
188- }
189-
190- private onBrushSelection ( event : D3BrushEvent < RenderableTopologyNode > ) : void {
191- if ( ! event . selection ) {
192- return ;
193- }
194-
195- const [ start , end ] = event . selection as [ [ number , number ] , [ number , number ] ] ;
196- if ( isEqual ( start , end ) ) {
197- return ;
198- }
199- const chartZoomScale = this . getZoomScale ( ) ;
200- const viewRect = {
201- top : start [ 1 ] * chartZoomScale ,
202- left : start [ 0 ] * chartZoomScale ,
203- bottom : end [ 1 ] * chartZoomScale ,
204- right : end [ 0 ] * chartZoomScale ,
205- width : end [ 0 ] * chartZoomScale - start [ 0 ] * chartZoomScale ,
206- height : end [ 1 ] * chartZoomScale - start [ 1 ] * chartZoomScale
207- } ;
208-
209- this . panToRect ( viewRect ) ;
210- }
211-
212- private translateToRect ( rect : ClientRect ) : void {
213- const centerX = rect . left + rect . width / 2 ;
214- const centerY = rect . top + rect . height / 2 ;
215- this . zoomBehavior . translateTo ( this . getContainerSelectionOrThrow ( ) , centerX , centerY ) ;
216- }
217-
218- private updateZoom ( transform : ZoomTransform ) : void {
219- this . getTargetSelectionOrThrow ( ) . attr ( 'transform' , transform . toString ( ) ) ;
220- this . zoomChangeSubject . next ( transform . k ) ;
221- }
222-
223- private checkValidZoomEvent ( receivedEvent : ZoomSourceEvent ) : boolean {
224- if ( this . isScrollEvent ( receivedEvent ) ) {
225- return this . isValidTriggerEvent ( receivedEvent , this . config && this . config . scroll ) ;
226- }
227- if ( this . isPrimaryMouseOrTouchEvent ( receivedEvent ) ) {
228- return this . isValidTriggerEvent ( receivedEvent , this . config && this . config . pan ) ;
229- }
230-
231- return false ;
232- }
233-
234- private isValidTriggerEvent (
235- receivedEvent : TouchEvent | MouseEvent ,
236- triggerConfig ?: TopologyEventTriggerConfig
237- ) : boolean {
238- if ( ! triggerConfig ) {
239- return false ;
240- }
241- if ( ! triggerConfig . requireModifiers ) {
242- return true ;
243- }
244-
245- return triggerConfig . requireModifiers . some ( key => this . eventHasModifier ( receivedEvent , key ) ) ;
246- }
24760
248- private eventHasModifier ( receivedEvent : TouchEvent | MouseEvent , modifier : ZoomEventKeyModifier ) : boolean {
249- switch ( modifier ) {
250- case Key . Control :
251- return receivedEvent . ctrlKey ;
252- case Key . Meta :
253- return receivedEvent . metaKey ;
254- default :
255- return false ;
256- }
257- }
258-
259- private isPrimaryMouseOrTouchEvent ( receivedEvent : ZoomSourceEvent ) : receivedEvent is TouchEvent | MouseEvent {
260- return (
261- ( 'TouchEvent' in window && receivedEvent instanceof TouchEvent ) ||
262- ( receivedEvent instanceof MouseEvent &&
263- ! this . isScrollEvent ( receivedEvent ) &&
264- receivedEvent . button === MouseButton . Primary )
265- ) ;
266- }
267-
268- private isScrollEvent ( receivedEvent : ZoomSourceEvent ) : receivedEvent is WheelEvent {
269- return receivedEvent instanceof WheelEvent ;
270- }
271-
272- private updateDraggingClassIfNeeded ( zoomEvent : ZoomHandlerEvent ) : void {
273- this . getContainerSelectionOrThrow ( ) . classed ( 'dragging' , this . isPanStartEvent ( zoomEvent ) ) ;
274- }
275-
276- private isPanStartEvent ( zoomEvent : ZoomHandlerEvent ) : boolean {
277- return zoomEvent . type === 'start' && this . isPrimaryMouseOrTouchEvent ( zoomEvent . sourceEvent ) ;
278- }
279-
280- private getTargetSelectionOrThrow ( ) : Selection < TTarget , unknown , null , unknown > {
281- return throwIfNil ( this . config ) . target ;
282- }
61+ const overlayZoomScale = this . determineZoomScale ( nodes , boundingBox ) ;
28362
284- private getContainerSelectionOrThrow ( ) : Selection < TContainer , unknown , null , unknown > {
285- return throwIfNil ( this . config ) . container ;
63+ this . showBrushOverlay ( overlayZoomScale ) ;
28664 }
28765}
28866
289- type ZoomEventKeyModifier = Key . Control | Key . Meta ;
290- // Type ZoomSourceEventType = 'wheel' | 'mousedown' | 'mouseup' | 'mousemove';
291- type ZoomSourceEvent = MouseEvent | TouchEvent | null ;
292- interface ZoomHandlerEvent extends D3ZoomEvent < Element , unknown > {
293- sourceEvent : ZoomSourceEvent ;
294- type : 'start' | 'zoom' | 'end' ;
295- }
296-
297- export interface TopologyZoomConfiguration < TContainer extends Element , TTarget extends Element > {
298- container : Selection < TContainer , unknown , null , undefined > ;
299- target : Selection < TTarget , unknown , null , undefined > ;
300- brushOverlay ?: Selection < SVGGElement , unknown , null , undefined > ;
301- scroll ?: TopologyEventTriggerConfig ;
302- pan ?: TopologyEventTriggerConfig ;
303- minScale ?: number ;
304- maxScale ?: number ;
305- }
306-
307- interface TopologyEventTriggerConfig {
308- requireModifiers ?: ZoomEventKeyModifier [ ] ;
309- }
67+ export interface TopologyZoomConfiguration < TContainer extends Element , TTarget extends Element >
68+ extends D3ZoomConfiguration < TContainer , TTarget > { }
0 commit comments