import * as _ from 'underscore' ; import * as d3 from 'd3' ; import { Scale } from 'bqscales' ; const d3GetEvent = function ( ) { return require ( 'd3-selection' ) . event ; } . bind ( this ) ; import { Mark } from './Mark' ; import { applyStyles } from './utils' ; export abstract class ScatterBase extends Mark { render ( ) { const base_creation_promise = super . render ( ) ; const that = this ; this . drag_listener = d3 . drag ( ) . subject ( ( d ) => { return { x : that . x_scale . scale ( d . x ) , y : that . y_scale . scale ( d . y ) } ; } ) . on ( 'start' , function ( d , i ) { return that . drag_start ( d , i , this ) ; } ) . on ( 'drag' , function ( d , i ) { return that . on_drag ( d , i , this ) ; } ) . on ( 'end' , function ( d , i ) { return that . drag_ended ( d , i , this ) ; } ) ; this . selected_style = this . model . get ( 'selected_style' ) ; this . unselected_style = this . model . get ( 'unselected_style' ) ; this . selected_indices = this . model . get ( 'selected' ) ; this . hovered_style = this . model . get ( 'hovered_style' ) ; this . unhovered_style = this . model . get ( 'unhovered_style' ) ; this . hovered_index = ! this . model . get ( 'hovered_point' ) ? null : [ this . model . get ( 'hovered_point' ) ] ; this . display_el_classes = [ 'dot' , 'legendtext' ] ; this . event_metadata = { mouse_over : { msg_name : 'hover' , lookup_data : false , hit_test : true , } , legend_clicked : { msg_name : 'legend_click' , hit_test : true , } , element_clicked : { msg_name : 'element_click' , lookup_data : false , hit_test : true , } , parent_clicked : { msg_name : 'background_click' , hit_test : false , } , } ; this . displayed . then ( ( ) => { this . parent . tooltip_div . node ( ) . appendChild ( this . tooltip_div . node ( ) ) ; this . create_tooltip ( ) ; } ) ; return base_creation_promise . then ( ( ) => { this . event_listeners = { } ; this . process_interactions ( ) ; this . create_listeners ( ) ; this . compute_view_padding ( ) ; this . draw ( ) ; } ) ; } set_ranges ( ) { const x_scale = this . scales . x , y_scale = this . scales . y , size_scale = this . scales . size , opacity_scale = this . scales . opacity , skew_scale = this . scales . skew , rotation_scale = this . scales . rotation ; if ( x_scale ) { x_scale . setRange ( this . parent . padded_range ( 'x' , x_scale . model ) ) ; } if ( y_scale ) { y_scale . setRange ( this . parent . padded_range ( 'y' , y_scale . model ) ) ; } if ( size_scale ) { size_scale . setRange ( [ 0 , this . model . get ( 'default_size' ) ] ) ; } if ( opacity_scale ) { opacity_scale . setRange ( [ 0.2 , 1 ] ) ; } if ( skew_scale ) { skew_scale . setRange ( [ 0 , 1 ] ) ; } if ( rotation_scale ) { rotation_scale . setRange ( [ 0 , 180 ] ) ; } } set_positional_scales ( ) { this . x_scale = this . scales . x ; this . y_scale = this . scales . y ; if ( ! this . x_scale ) { this . x_scale = this . parent . scale_x ; } if ( ! this . y_scale ) { this . y_scale = this . parent . scale_y ; } this . listenTo ( this . x_scale , 'domain_changed' , ( ) => { if ( ! this . model . dirty ) { const animate = true ; this . update_position ( animate ) ; } } ) ; this . listenTo ( this . y_scale , 'domain_changed' , ( ) => { if ( ! this . model . dirty ) { const animate = true ; this . update_position ( animate ) ; } } ) ; } initialize_additional_scales ( ) { const color_scale = this . scales . color , size_scale = this . scales . size , opacity_scale = this . scales . opacity , skew_scale = this . scales . skew , rotation_scale = this . scales . rotation ; if ( color_scale ) { this . listenTo ( color_scale , 'domain_changed' , ( ) => { const animate = true ; if ( ! this . model . dirty ) { this . color_scale_updated ( animate ) ; } } ) ; color_scale . on ( 'color_scale_range_changed' , this . color_scale_updated , this ) ; } if ( size_scale ) { this . listenTo ( size_scale , 'domain_changed' , ( ) => { const animate = true ; this . update_default_size ( animate ) ; } ) ; } if ( opacity_scale ) { this . listenTo ( opacity_scale , 'domain_changed' , ( ) => { const animate = true ; this . update_opacities ( animate ) ; } ) ; } if ( skew_scale ) { this . listenTo ( skew_scale , 'domain_changed' , ( ) => { const animate = true ; this . update_default_skew ( animate ) ; } ) ; } if ( rotation_scale ) { this . listenTo ( rotation_scale , 'domain_changed' , ( ) => { const animate = true ; this . update_position ( animate ) ; } ) ; } } create_listeners ( ) { super . create_listeners ( ) ; this . d3el . on ( 'mouseover' , ( ) => { this . event_dispatcher ( 'mouse_over' ) ; } ) . on ( 'mousemove' , ( ) => { this . event_dispatcher ( 'mouse_move' ) ; } ) . on ( 'mouseout' , ( ) => { this . event_dispatcher ( 'mouse_out' ) ; } ) ; this . listenTo ( this . model , 'data_updated' , ( ) => { const animate = true ; this . draw ( animate ) ; } ) ; this . listenTo ( this . model , 'change:tooltip' , this . create_tooltip ) ; this . listenTo ( this . model , 'change:enable_hover' , ( ) => { this . hide_tooltip ( ) ; } ) ; this . listenTo ( this . model , 'change:interactions' , this . process_interactions ) ; this . listenTo ( this . model , 'change:enable_move' , this . set_drag_behavior ) ; this . listenTo ( this . model , 'change:selected' , this . update_selected ) ; this . listenTo ( this . model , 'change:hovered_point' , this . update_hovered ) ; this . listenTo ( this . model , 'change:hovered_style' , this . hovered_style_updated ) ; this . listenTo ( this . model , 'change:unhovered_style' , this . unhovered_style_updated ) ; this . listenTo ( this . parent , 'bg_clicked' , ( ) => { this . event_dispatcher ( 'parent_clicked' ) ; } ) ; } get_element_size ( data ) { const size_scale = this . scales . size ; if ( size_scale && data . size !== undefined ) { return size_scale . scale ( data . size ) ; } return this . model . get ( 'default_size' ) ; } get_element_opacity ( data , index ) { const opacity_scale = this . scales . opacity ; const opacities = this . model . get ( 'opacities' ) ; const len = opacities . length ; if ( opacity_scale && data . opacity !== undefined ) { return opacity_scale . scale ( data . opacity ) ; } return opacities [ index % len ] ; } get_element_skew ( data ) { const skew_scale = this . scales . skew ; if ( skew_scale && data . skew !== undefined ) { return skew_scale . scale ( data . skew ) ; } return this . model . get ( 'default_skew' ) ; } get_element_rotation ( d ) { const rotation_scale = this . scales . rotation ; return ! rotation_scale || ! d . rotation ? '' : 'rotate(' + rotation_scale . scale ( d . rotation ) + ')' ; } relayout ( ) { this . set_ranges ( ) ; this . update_position ( ) ; } update_position ( animate ? ) { const x_scale = this . scales . x , y_scale = this . scales . y ; const animation_duration = animate === true ? this . parent . model . get ( 'animation_duration' ) : 0 ; this . d3el . selectAll ( '.object_grp' ) . transition ( 'update_position' ) . duration ( animation_duration ) . attr ( 'transform' , ( d ) => { return ( 'translate(' + ( x_scale . scale ( d . x ) + x_scale . offset ) + ',' + ( y_scale . scale ( d . y ) + y_scale . offset ) + ')' + this . get_element_rotation ( d ) ) ; } ) ; this . x_pixels = this . model . mark_data . map ( ( el ) => { return x_scale . scale ( el . x ) + x_scale . offset ; } ) ; this . y_pixels = this . model . mark_data . map ( ( el ) => { return y_scale . scale ( el . y ) + y_scale . offset ; } ) ; this . pixel_coords = this . model . mark_data . map ( ( el ) => { return [ x_scale . scale ( el . x ) + x_scale . offset , y_scale . scale ( el . y ) + y_scale . offset , ] ; } ) ; } draw ( animate ? ) { this . set_ranges ( ) ; const elements = this . d3el . selectAll ( '.object_grp' ) . data ( this . model . mark_data , ( d ) => { return d . unique_id ; } ) ; const elements_added = elements . enter ( ) . append ( 'g' ) . attr ( 'class' , 'object_grp' ) ; this . update_position ( animate ) ; this . set_drag_behavior ( ) ; elements_added . on ( 'click' , ( d , i ) => { this . event_dispatcher ( 'element_clicked' , { data : d , index : i } ) ; } ) ; elements_added . on ( 'mouseover' , ( d , i ) => { this . scatter_hover_handler ( { data : d , index : i } ) ; } ) ; elements_added . on ( 'mouseout' , ( ) => { this . reset_hover_points ( ) ; } ) ; this . draw_elements ( animate , elements_added ) ; elements . exit ( ) . remove ( ) ; } draw_elements ( animate , elements_added ) { } process_click ( interaction ) { super . process_click ( interaction ) ; switch ( interaction ) { case 'add' : this . event_listeners . parent_clicked = this . add_element ; this . event_listeners . element_clicked = ( ) => { } ; break ; case 'delete' : this . event_listeners . parent_clicked = ( ) => { } ; this . event_listeners . element_clicked = this . delete_element ; break ; case 'select' : this . event_listeners . parent_clicked = this . reset_selection ; this . event_listeners . element_clicked = this . scatter_click_handler ; break ; } } reset_hover_points ( ) { this . model . set ( 'hovered_point' , null ) ; this . hovered_index = null ; this . touch ( ) ; } scatter_hover_handler ( args ) { const index = args . index ; this . model . set ( 'hovered_point' , index , { updated_view : this } ) ; this . touch ( ) ; } reset_selection ( ) { this . model . set ( 'selected' , null ) ; this . selected_indices = null ; this . touch ( ) ; } scatter_click_handler ( args ) { const index = args . index ; const idx = this . model . get ( 'selected' ) || [ ] ; let selected = Array . from ( idx ) ; const elem_index = selected . indexOf ( index ) ; const accelKey = d3GetEvent ( ) . ctrlKey || d3GetEvent ( ) . metaKey ; if ( elem_index > - 1 && accelKey ) { selected . splice ( elem_index , 1 ) ; } else { if ( accelKey ) { selected . push ( index ) ; } else { selected = [ ] ; selected . push ( index ) ; } } this . model . set ( 'selected' , selected . length === 0 ? null : new Uint32Array ( selected ) , { updated_view : this } ) ; this . touch ( ) ; let e = d3GetEvent ( ) ; if ( ! e ) { e = window . event ; } if ( e . cancelBubble !== undefined ) { e . cancelBubble = true ; } if ( e . stopPropagation ) { e . stopPropagation ( ) ; } e . preventDefault ( ) ; } hovered_style_updated ( model , style ) { this . hovered_style = style ; this . clear_style ( model . previous ( 'hovered_style' ) , this . hovered_index ) ; this . style_updated ( style , this . hovered_index ) ; } unhovered_style_updated ( model , style ) { this . unhovered_style = style ; const hov_indices = this . hovered_index ; const unhovered_indices = hov_indices ? _ . range ( this . model . mark_data . length ) . filter ( ( index ) => { return hov_indices . indexOf ( index ) === - 1 ; } ) : [ ] ; this . clear_style ( model . previous ( 'unhovered_style' ) , unhovered_indices ) ; this . style_updated ( style , unhovered_indices ) ; } draw_legend ( elem , x_disp , y_disp , inter_x_disp , inter_y_disp ) { this . legend_el = elem . selectAll ( '.legend' + this . uuid ) . data ( [ this . model . mark_data [ 0 ] ] ) ; const colors = this . model . get ( 'colors' ) , len = colors . length ; const rect_dim = inter_y_disp * 0.8 ; const legend = this . legend_el . enter ( ) . append ( 'g' ) . attr ( 'class' , 'legend' + this . uuid ) . attr ( 'transform' , ( d , i ) => { return 'translate(0, ' + ( i * inter_y_disp + y_disp ) + ')' ; } ) . on ( 'mouseover' , ( ) => { this . event_dispatcher ( 'legend_mouse_over' ) ; } ) . on ( 'mouseout' , ( ) => { this . event_dispatcher ( 'legend_mouse_out' ) ; } ) . on ( 'click' , ( ) => { this . event_dispatcher ( 'legend_clicked' ) ; } ) ; this . draw_legend_elements ( legend , rect_dim ) ; legend . append ( 'text' ) . attr ( 'class' , 'legendtext' ) . attr ( 'x' , rect_dim * 1.2 ) . attr ( 'y' , rect_dim / 2 ) . attr ( 'dy' , '0.35em' ) . text ( ( d , i ) => { return this . model . get ( 'labels' ) [ i ] ; } ) . style ( 'fill' , ( d , i ) => { return colors [ i % len ] ; } ) ; this . legend_el = legend . merge ( this . legend_el ) ; const max_length = d3 . max ( this . model . get ( 'labels' ) , ( d ) => { return Number ( d . length ) ; } ) ; this . legend_el . exit ( ) . remove ( ) ; return [ 1 , max_length ] ; } draw_legend_elements ( elements_added , rect_dim ) { } invert_point ( pixel ) { if ( pixel === undefined ) { this . model . set ( 'selected' , null ) ; this . touch ( ) ; return ; } const abs_diff = this . x_pixels . map ( ( elem ) => { return Math . abs ( elem - pixel ) ; } ) ; const sel_index = abs_diff . indexOf ( d3 . min ( abs_diff ) ) ; this . model . set ( 'selected' , new Uint32Array ( [ sel_index ] ) ) ; this . touch ( ) ; } selector_changed ( point_selector , rect_selector ) { if ( point_selector === undefined ) { this . model . set ( 'selected' , null ) ; this . touch ( ) ; return [ ] ; } const pixels = this . pixel_coords ; const indices = new Uint32Array ( _ . range ( pixels . length ) ) ; const selected = indices . filter ( ( index ) => { return point_selector ( pixels [ index ] ) ; } ) ; this . model . set ( 'selected' , selected ) ; this . touch ( ) ; } update_selected ( model , value ) { this . selected_indices = value ; this . apply_styles ( ) ; } update_hovered ( model , value ) { this . hovered_index = value === null ? value : [ value ] ; this . apply_styles ( ) ; } apply_styles ( style_arr ? ) { if ( style_arr === undefined || style_arr == null ) { style_arr = [ this . selected_style , this . unselected_style , this . hovered_style , this . unhovered_style , ] ; } super . apply_styles ( [ style_arr ] ) ; const all_indices = _ . range ( this . model . mark_data . length ) ; this . set_style_on_elements ( this . hovered_style , this . hovered_index ) ; const unhovered_indices = ! this . hovered_index ? [ ] : _ . difference ( all_indices , this . hovered_index ) ; this . set_style_on_elements ( this . unhovered_style , unhovered_indices ) ; } clear_style ( style_dict , indices ) { let elements = this . d3el . selectAll ( '.element' ) ; if ( indices ) { elements = elements . filter ( ( d , index ) => { return indices . indexOf ( index ) !== - 1 ; } ) ; } const clearing_style = { } ; for ( const key in style_dict ) { clearing_style [ key ] = null ; } applyStyles ( elements , clearing_style ) ; } set_style_on_elements ( style , indices ) { if ( ! indices || indices . length === 0 ) { return ; } if ( Object . keys ( style ) . length === 0 ) { return ; } let elements = this . d3el . selectAll ( '.element' ) ; elements = elements . filter ( ( data , index ) => { return indices . indexOf ( index ) !== - 1 ; } ) ; applyStyles ( elements , style ) ; } compute_view_padding ( ) { const xPadding = Math . sqrt ( this . model . get ( 'default_size' ) ) / 2 + 1.0 ; if ( xPadding !== this . xPadding || xPadding !== this . yPadding ) { this . xPadding = xPadding ; this . yPadding = xPadding ; this . trigger ( 'mark_padding_updated' ) ; } } update_array ( d , i ) { const x_scale = this . scales . x , y_scale = this . scales . y ; if ( ! this . model . get ( 'restrict_y' ) ) { const x = Object . assign ( this . model . get ( 'x' ) . slice ( ) , this . model . get ( 'x' ) ) ; x [ i ] = x_scale . invert ( d [ 0 ] ) ; this . model . set ( 'x' , x ) ; } if ( ! this . model . get ( 'restrict_x' ) ) { const y = Object . assign ( this . model . get ( 'y' ) . slice ( ) , this . model . get ( 'y' ) ) ; y [ i ] = y_scale . invert ( d [ 1 ] ) ; this . model . set ( 'y' , y ) ; } this . touch ( ) ; } set_drag_behavior ( ) { const elements = this . d3el . selectAll ( '.object_grp' ) ; if ( this . model . get ( 'enable_move' ) ) { elements . call ( this . drag_listener ) ; } else { elements . on ( '.drag' , null ) ; } } set_drag_style ( d , i , dragged_node ) { } reset_drag_style ( d , i , dragged_node ) { } drag_start ( d , i , dragged_node ) { const x_scale = this . scales . x , y_scale = this . scales . y ; d [ 0 ] = x_scale . scale ( d . x ) + x_scale . offset ; d [ 1 ] = y_scale . scale ( d . y ) + y_scale . offset ; this . set_drag_style ( d , i , dragged_node ) ; this . send ( { event : 'drag_start' , point : { x : d . x , y : d . y } , index : i , } ) ; } on_drag ( d , i , dragged_node ) { const x_scale = this . scales . x , y_scale = this . scales . y ; const restrict_x = this . model . get ( 'restrict_x' ) , restrict_y = this . model . get ( 'restrict_y' ) ; if ( restrict_x && restrict_y ) { return ; } if ( ! restrict_y ) { d [ 0 ] = d3GetEvent ( ) . x ; } if ( ! restrict_x ) { d [ 1 ] = d3GetEvent ( ) . y ; } d3 . select ( dragged_node ) . attr ( 'transform' , ( ) => { return 'translate(' + d [ 0 ] + ',' + d [ 1 ] + ')' ; } ) ; this . send ( { event : 'drag' , origin : { x : d . x , y : d . y } , point : { x : x_scale . invert ( d [ 0 ] ) , y : y_scale . invert ( d [ 1 ] ) , } , index : i , } ) ; if ( this . model . get ( 'update_on_move' ) ) { this . update_array ( d , i ) ; } } drag_ended ( d , i , dragged_node ) { const x_scale = this . scales . x , y_scale = this . scales . y ; this . reset_drag_style ( d , i , dragged_node ) ; this . update_array ( d , i ) ; this . send ( { event : 'drag_end' , point : { x : x_scale . invert ( d [ 0 ] ) , y : y_scale . invert ( d [ 1 ] ) , } , index : i , } ) ; } add_element ( ) { const mouse_pos = d3 . mouse ( this . el ) ; const curr_pos = [ mouse_pos [ 0 ] , mouse_pos [ 1 ] ] ; const x_scale = this . scales . x , y_scale = this . scales . y ; const x = this . model . get ( 'x' ) ; const y = this . model . get ( 'y' ) ; const xn = new x . constructor ( x . length + 1 ) ; const yn = new y . constructor ( y . length + 1 ) ; xn . set ( x ) ; yn . set ( y ) ; xn [ x . length ] = x_scale . invert ( curr_pos [ 0 ] ) ; yn [ y . length ] = y_scale . invert ( curr_pos [ 1 ] ) ; this . model . set ( 'x' , xn ) ; this . model . set ( 'y' , yn ) ; this . touch ( ) ; } delete_element ( args ) { const index = args . index ; let x = Object . assign ( this . model . get ( 'x' ) . slice ( ) , this . model . get ( 'x' ) ) ; let y = Object . assign ( this . model . get ( 'y' ) . slice ( ) , this . model . get ( 'y' ) ) ; x . copyWithin ( index , index + 1 , x . length ) ; y . copyWithin ( index , index + 1 , y . length ) ; x = x . slice ( 0 , x . length - 1 ) ; y = y . slice ( 0 , y . length - 1 ) ; this . model . set ( 'x' , x ) ; this . model . set ( 'y' , y ) ; this . touch ( ) ; } abstract color_scale_updated ( animate ? ) ; abstract update_opacities ( animate ? ) ; abstract update_default_skew ( animate ? ) ; abstract update_default_size ( animate ? ) ; hovered_index : number [ ] ; hovered_style : { [ key ] : string ; } ; unhovered_style : { [ key ] : string ; } ; drag_listener : d3 . DragBehavior < Element , unknown , unknown > ; pixel_coords : number [ ] ; legend_el : any ; x_pixels : number [ ] ; y_pixels : number [ ] ; x_scale : Scale ; y_scale : Scale ; }