import * as d3 from 'd3' ; import { d3GetEvent } from './utils' ; import * as _ from 'underscore' ; import * as selector from './Selector' ; import * as utils from './utils' ; import * as sel_utils from './selector_utils' ; import { Mark } from './Mark' ; function point_selector ( x , y ) { return function ( xar , yar ) { if ( typeof yar === 'undefined' ) { return sel_utils . point_in_rectangle ( xar , x , y ) ; } const len = Math . min ( xar . length , yar . length ) ; const mask = new Uint8Array ( len ) ; if ( x . length && y . length ) { for ( let i = 0 ; i < len ; i ++ ) { mask [ i ] = x [ 0 ] <= xar [ i ] && xar [ i ] <= x [ 1 ] && y [ 0 ] <= yar [ i ] && yar [ i ] <= y [ 1 ] ? 1 : 0 ; } return mask ; } else if ( x . length ) { for ( let i = 0 ; i < len ; i ++ ) { mask [ i ] = x [ 0 ] <= xar [ i ] && xar [ i ] <= x [ 1 ] ? 1 : 0 ; } } else { for ( let i = 0 ; i < len ; i ++ ) { mask [ i ] = y [ 0 ] <= yar [ i ] && yar [ i ] <= y [ 1 ] ? 1 : 0 ; } } return mask ; } ; } function rect_selector ( x , y ) { return function ( xy ) { return sel_utils . rect_inter_rect ( xy [ 0 ] , xy [ 1 ] , x , y ) ; } ; } function sort ( a , b ) { return a - b ; } function update_mark_selected ( brush , extent_x ? , extent_y ? ) { if ( extent_x === undefined || extent_x . length === 0 ) { _ . each ( brush . mark_views , ( mark_view ) => { return mark_view . selector_changed ( ) ; } ) ; return ; } let x , y ; if ( extent_y === undefined ) { const orient = brush . model . get ( 'orientation' ) ; ( x = orient == 'vertical' ? [ ] : extent_x ) , ( y = orient == 'vertical' ? extent_x : [ ] ) ; } else { ( x = extent_x ) , ( y = extent_y ) ; } if ( x . length ) { x . sort ( sort ) ; } if ( y . length ) { y . sort ( sort ) ; } _ . each ( brush . mark_views , ( mark_view ) => { mark_view . selector_changed ( point_selector ( x , y ) , rect_selector ( x , y ) ) ; } ) ; } function adjust_rectangle ( brush ) { if ( brush . model . get ( 'orientation' ) == 'vertical' ) { brush . d3el . selectAll ( 'rect' ) . attr ( 'x' , 0 ) . attr ( 'width' , brush . width ) ; } else { brush . d3el . selectAll ( 'rect' ) . attr ( 'y' , 0 ) . attr ( 'height' , brush . height ) ; } } abstract class BrushMixinXYSelector extends selector . BaseXYSelector { brush_render ( ) { this . brushing = false ; } color_change ( ) { if ( this . model . get ( 'color' ) !== null ) { this . d3el . selectAll ( '.selection' ) . style ( 'fill' , this . model . get ( 'color' ) ) ; } } brush_start ( ) { if ( this . ignoreBrushEvents ) { return ; } this . brushing = true ; this . model . set ( 'brushing' , true ) ; this . touch ( ) ; } brush_move ( ) { if ( this . ignoreBrushEvents ) { return ; } this . convert_and_save ( ) ; } brush_end ( ) { if ( this . ignoreBrushEvents ) { return ; } this . model . set ( 'brushing' , false ) ; this . convert_and_save ( ) ; this . brushing = false ; } adjust_rectangle ( ) { adjust_rectangle ( this ) ; } update_mark_selected ( extent_x ? , extent_y ? ) { return update_mark_selected ( this , extent_x , extent_y ) ; } abstract convert_and_save ( extent ? , item ? ) ; brush : d3 . BrushBehavior < any > ; brushing : boolean ; mark_views : Mark [ ] ; ignoreBrushEvents = false ; } abstract class BrushMixinXSelector extends selector . BaseXSelector { brush_render ( ) { this . brushing = false ; } color_change ( ) { if ( this . model . get ( 'color' ) !== null ) { this . d3el . selectAll ( '.selection' ) . style ( 'fill' , this . model . get ( 'color' ) ) ; } } brush_start ( ) { if ( this . ignoreBrushEvents ) { return ; } this . brushing = true ; this . model . set ( 'brushing' , true ) ; this . touch ( ) ; } brush_move ( ) { if ( this . ignoreBrushEvents ) { return ; } this . convert_and_save ( ) ; } brush_end ( ) { if ( this . ignoreBrushEvents ) { return ; } this . model . set ( 'brushing' , false ) ; this . convert_and_save ( ) ; this . brushing = false ; } adjust_rectangle ( ) { adjust_rectangle ( this ) ; } update_mark_selected ( extent_x ? , extent_y ? ) { return update_mark_selected ( this , extent_x , extent_y ) ; } abstract convert_and_save ( extent ? , item ? ) ; brush : d3 . BrushBehavior < any > ; brushing : boolean ; mark_views : Mark [ ] ; ignoreBrushEvents = false ; } export class BrushSelector extends BrushMixinXYSelector { async render ( ) { await super . render ( ) ; this . brush_render ( ) ; await this . create_scales ( ) ; await this . mark_views_promise ; this . brush = d3 . brush ( ) . on ( 'start' , _ . bind ( this . brush_start , this ) ) . on ( 'brush' , _ . bind ( this . brush_move , this ) ) . on ( 'end' , _ . bind ( this . brush_end , this ) ) ; this . brush . extent ( [ [ 0 , 0 ] , [ this . width , this . height ] , ] ) ; this . d3el . attr ( 'class' , 'selector brushintsel' ) ; this . d3el . call ( this . brush ) ; this . adjust_rectangle ( ) ; this . color_change ( ) ; this . create_listeners ( ) ; this . selected_changed ( ) ; } create_listeners ( ) { super . create_listeners ( ) ; this . listenTo ( this . model , 'change:color' , this . color_change ) ; this . listenTo ( this . model , 'change:selected_x' , this . selected_changed ) ; this . listenTo ( this . model , 'change:selected_y' , this . selected_changed ) ; } empty_selection ( ) { this . update_mark_selected ( ) ; this . model . set ( 'selected_x' , null ) ; this . model . set ( 'selected_y' , null ) ; this . touch ( ) ; } convert_and_save ( ) { const e = d3GetEvent ( ) ; if ( ! e . sourceEvent ) { return ; } if ( ! e . selection ) { this . empty_selection ( ) ; } else { const d0 = e . selection ; const pixel_extent_x = [ d0 [ 0 ] [ 0 ] , d0 [ 1 ] [ 0 ] ] ; const pixel_extent_y = [ d0 [ 1 ] [ 1 ] , d0 [ 0 ] [ 1 ] ] ; const extent_x = pixel_extent_x . map ( this . x_scale . invert . bind ( this . x_scale ) ) . sort ( sort ) ; const extent_y = pixel_extent_y . map ( this . y_scale . invert . bind ( this . y_scale ) ) . sort ( sort ) ; this . update_mark_selected ( pixel_extent_x , pixel_extent_y ) ; this . set_selected ( 'selected_x' , this . x_scale . model . typedRange ( extent_x as number [ ] ) ) ; this . set_selected ( 'selected_y' , this . y_scale . model . typedRange ( extent_y as number [ ] ) ) ; this . touch ( ) ; } } selected_changed ( ) { if ( this . brushing ) { return ; } const selected_x = this . model . get ( 'selected_x' ) || [ ] , selected_y = this . model . get ( 'selected_y' ) || [ ] ; if ( selected_x . length === 0 || selected_y . length === 0 ) { this . update_mark_selected ( ) ; } else if ( selected_x . length != 2 || selected_y . length != 2 ) { return ; } else { const pixel_extent_x = selected_x . map ( ( v ) => this . x_scale . offset + this . x_scale . scale ( v ) ) . sort ( sort ) ; const pixel_extent_y = selected_y . map ( ( v ) => this . y_scale . offset + this . y_scale . scale ( v ) ) . sort ( sort ) ; this . update_mark_selected ( pixel_extent_x , pixel_extent_y ) ; } this . syncModelToBrush ( ) ; } relayout ( ) { super . relayout ( ) ; this . d3el . select ( '.background' ) . attr ( 'width' , this . width ) . attr ( 'height' , this . height ) ; this . set_x_range ( [ this . x_scale ] ) ; this . set_y_range ( [ this . y_scale ] ) ; this . brush . extent ( [ [ 0 , 0 ] , [ this . width , this . height ] , ] ) ; this . syncModelToBrush ( ) ; } private syncModelToBrush ( ) { this . ignoreBrushEvents = true ; try { if ( this . model . get ( 'selected_x' ) && this . model . get ( 'selected_y' ) ) { const range_x = this . model . get ( 'selected_x' ) . map ( ( v ) => this . x_scale . offset + this . x_scale . scale ( v ) ) . sort ( sort ) ; const range_y = this . model . get ( 'selected_y' ) . map ( ( v ) => this . y_scale . offset + this . y_scale . scale ( v ) ) . sort ( sort ) ; this . brush . move ( this . d3el , [ [ range_x [ 0 ] , range_y [ 0 ] ] , [ range_x [ 1 ] , range_y [ 1 ] ] , ] ) ; } else { this . brush . move ( this . d3el , null ) ; } this . d3el . call ( this . brush ) ; } finally { this . ignoreBrushEvents = false ; } this . d3el . call ( this . brush ) ; } adjust_rectangle ( ) { } reset ( ) { } d3el : d3 . Selection < SVGGElement , any , any , any > ; } export class BrushIntervalSelector extends BrushMixinXSelector { async render ( ) { await super . render ( ) ; this . brush_render ( ) ; await this . mark_views_promise ; await this . create_scales ( ) ; this . brush = ( this . model . get ( 'orientation' ) == 'vertical' ? d3 . brushY ( ) : d3 . brushX ( ) ) . on ( 'start' , _ . bind ( this . brush_start , this ) ) . on ( 'brush' , _ . bind ( this . brush_move , this ) ) . on ( 'end' , _ . bind ( this . brush_end , this ) ) ; this . brush . extent ( [ [ 0 , 0 ] , [ this . width , this . height ] , ] ) ; this . d3el . attr ( 'class' , 'selector brushintsel' ) ; this . d3el . call ( this . brush ) ; this . adjust_rectangle ( ) ; this . color_change ( ) ; this . create_listeners ( ) ; this . selected_changed ( ) ; } create_listeners ( ) { super . create_listeners ( ) ; this . listenTo ( this . model , 'change:color' , this . color_change ) ; this . listenTo ( this . model , 'change:selected' , this . selected_changed ) ; } empty_selection ( ) { this . update_mark_selected ( ) ; this . model . set ( 'selected' , null ) ; this . touch ( ) ; } convert_and_save ( ) { const e = d3GetEvent ( ) ; if ( ! e . sourceEvent ) { return ; } if ( ! e . selection ) { this . empty_selection ( ) ; } else { const pixel_extent = e . selection ; const extent = pixel_extent . map ( this . scale . invert . bind ( this . scale ) ) . sort ( sort ) ; this . update_mark_selected ( pixel_extent ) ; this . set_selected ( 'selected' , this . scale . model . typedRange ( extent ) ) ; this . touch ( ) ; } } update_scale_domain ( ignore_gui_update ) { super . update_scale_domain ( ) ; if ( ignore_gui_update !== true ) { this . selected_changed ( ) ; } } selected_changed ( ) { if ( this . brushing ) { return ; } const selected = this . model . get ( 'selected' ) || [ ] ; if ( selected . length === 0 ) { this . update_mark_selected ( ) ; } else if ( selected . length != 2 ) { return ; } else { const extent = [ selected [ 0 ] , selected [ 1 ] ] ; const pixel_extent = extent . map ( ( v ) => this . scale . scale ( v ) ) . sort ( sort ) ; this . update_mark_selected ( pixel_extent ) ; } this . syncModelToBrush ( ) ; } relayout ( ) { super . relayout ( ) ; this . adjust_rectangle ( ) ; this . d3el . select ( '.background' ) . attr ( 'width' , this . width ) . attr ( 'height' , this . height ) ; this . set_range ( [ this . scale ] ) ; this . brush . extent ( [ [ 0 , 0 ] , [ this . width , this . height ] , ] ) ; this . syncModelToBrush ( ) ; } private syncModelToBrush ( ) { this . ignoreBrushEvents = true ; try { if ( this . model . get ( 'selected' ) ) { const range = this . model . get ( 'selected' ) . map ( ( v ) => this . scale . scale ( v ) ) . sort ( sort ) ; this . brush . move ( this . d3el , range ) ; } else { this . brush . move ( this . d3el , null ) ; } this . d3el . call ( this . brush ) ; } finally { this . ignoreBrushEvents = false ; } } reset ( ) { } d3el : d3 . Selection < SVGGElement , any , any , any > ; } function add_remove_classes ( selection , add_classes , remove_classes ) { if ( remove_classes ) { remove_classes . forEach ( ( r_class ) => { selection . classed ( r_class , false ) ; } ) ; } if ( add_classes ) { add_classes . forEach ( ( a_class ) => { selection . classed ( a_class , true ) ; } ) ; } } export class MultiSelector extends BrushMixinXSelector { render ( ) { super . render . apply ( this ) ; this . brush_render ( ) ; this . names = this . model . get ( 'names' ) ; this . curr_index = 0 ; const scale_creation_promise = this . create_scales ( ) ; Promise . all ( [ this . mark_views_promise , scale_creation_promise ] ) . then ( ( ) => { this . d3el . attr ( 'class' , 'multiselector' ) ; this . d3el . attr ( 'width' , this . width ) ; this . d3el . attr ( 'height' , this . height ) ; this . create_brush ( ) ; this . selecting_brush = false ; this . create_listeners ( ) ; } ) ; } create_listeners ( ) { super . create_listeners ( ) ; this . listenTo ( this . model , 'change:names' , this . labels_change ) ; this . listenTo ( this . model , 'change:color' , this . color_change ) ; } labels_change ( model , value ) { const prev_names = model . previous ( 'names' ) ; this . names = value ; const data = _ . range ( this . curr_index + 1 ) ; const selected = utils . deepCopy ( this . model . get ( 'selected' ) ) ; data . forEach ( ( elem ) => { const label = this . get_label ( elem ) ; const prev_label = this . get_label ( elem , prev_names ) ; if ( prev_label !== label ) { this . d3el . select ( '.brush_text_' + elem ) . text ( label ) ; selected [ label ] = selected [ prev_label ] ; delete selected [ prev_label ] ; } } ) ; this . set_selected ( '_selected' , selected ) ; this . touch ( ) ; } create_brush ( ) { const index = this . curr_index ; const vertical = this . model . get ( 'orientation' ) == 'vertical' ; const brush = ( vertical ? d3 . brushY ( ) : d3 . brushX ( ) ) . on ( 'start' , ( ) => { this . brush_start ( ) ; } ) . on ( 'brush' , ( ) => { this . multi_brush_move ( index ) ; } ) . on ( 'end' , ( ) => { this . multi_brush_end ( index ) ; } ) ; brush . extent ( [ [ 0 , 0 ] , [ this . width , this . height ] , ] ) ; const new_brush_g = this . d3el . append ( 'g' ) . attr ( 'class' , 'selector brushintsel active' ) ; new_brush_g . append ( 'text' ) . text ( this . get_label ( this . curr_index ) ) . attr ( 'class' , 'brush_text_' + this . curr_index ) . style ( 'display' , 'none' ) ; if ( this . model . get ( 'orientation' ) == 'vertical' ) { new_brush_g . select ( 'text' ) . attr ( 'x' , 30 ) ; } else { new_brush_g . select ( 'text' ) . attr ( 'y' , 30 ) ; } new_brush_g . call ( brush ) ; this . color_change ( ) ; this . adjust_rectangle ( ) ; this . reset_handler ( new_brush_g ) ; this . brushes [ this . curr_index ] = brush ; this . brush_g [ this . curr_index ] = new_brush_g ; this . curr_index = this . curr_index + 1 ; } reset_handler ( brush_g ) { const that = this ; const old_handler = brush_g . on ( 'mousedown.brush' ) ; brush_g . on ( 'mousedown.brush' , function ( ) { const accelKey = d3GetEvent ( ) . ctrlKey || d3GetEvent ( ) . metaKey ; if ( d3GetEvent ( ) . shiftKey && accelKey ) { that . reset ( ) ; } else if ( accelKey ) { add_remove_classes ( d3 . select ( this ) , [ 'inactive' ] , [ 'active' ] ) ; that . create_brush ( ) ; } else if ( d3GetEvent ( ) . shiftKey && that . selecting_brush === false ) { add_remove_classes ( that . d3el . selectAll ( '.selector' ) , [ 'visible' ] , [ 'active' , 'inactive' ] ) ; that . selecting_brush = true ; } else { add_remove_classes ( that . d3el . selectAll ( '.selector' ) , [ 'inactive' ] , [ 'visible' ] ) ; add_remove_classes ( d3 . select ( this ) , [ 'active' ] , [ 'inactive' ] ) ; old_handler . call ( this ) ; that . selecting_brush = false ; } } ) ; } get_label ( index , arr ? ) { if ( arr === undefined || arr === null ) { arr = this . names ; } return arr . length > index ? arr [ index ] : index ; } multi_brush_move ( item ) { if ( this . ignoreBrushEvents ) { return ; } const extent = d3GetEvent ( ) . selection ; this . update_text ( item , extent ) ; this . convert_and_save ( extent , item ) ; } update_text ( item , extent ) { if ( extent === null ) { this . d3el . select ( '.brush_text_' + item ) . style ( 'display' , 'none' ) ; return ; } const orient = this . model . get ( 'orientation' ) == 'vertical' ? 'y' : 'x' ; const hide_names = ! this . model . get ( 'show_names' ) ; const mid = ( extent [ 0 ] + extent [ 1 ] ) / 2 ; this . d3el . select ( '.brush_text_' + item ) . style ( 'display' , hide_names ? 'none' : 'inline' ) . attr ( orient , mid ) ; } multi_brush_end ( item ) { if ( this . ignoreBrushEvents ) { return ; } const sel = d3GetEvent ( ) . selection ; this . model . set ( 'brushing' , false ) ; this . convert_and_save ( sel , item ) ; this . brushing = false ; } reset ( ) { this . d3el . selectAll ( '.selector' ) . remove ( ) ; this . model . set ( '_selected' , { } ) ; this . curr_index = 0 ; this . brushes = [ ] ; this . brush_g = [ ] ; this . touch ( ) ; this . create_brush ( ) ; } convert_and_save ( extent , item ) { if ( ! extent ) { this . update_mark_selected ( ) ; this . model . set ( '_selected' , { } ) ; } else { const selected = utils . deepCopy ( this . model . get ( '_selected' ) ) ; selected [ this . get_label ( item ) ] = extent . map ( this . scale . invert . bind ( this . scale ) ) ; this . update_mark_selected ( extent ) ; this . model . set ( '_selected' , selected ) ; } this . touch ( ) ; } selected_changed ( ) { } relayout ( ) { super . relayout ( ) ; this . adjust_rectangle ( ) ; this . d3el . select ( '.background' ) . attr ( 'width' , this . width ) . attr ( 'height' , this . height ) ; this . set_range ( [ this . scale ] ) ; this . brushes . forEach ( ( brush ) => { brush . extent ( [ [ 0 , 0 ] , [ this . width , this . height ] , ] ) ; } ) ; this . syncModelToBrush ( ) ; } private syncModelToBrush ( ) { this . ignoreBrushEvents = true ; try { const selected = this . model . get ( '_selected' ) ; this . brushes . forEach ( ( brush , index ) => { const brushSelected = selected [ this . get_label ( index ) ] ; if ( brushSelected ) { const range = brushSelected . map ( ( v ) => this . scale . scale ( v ) ) . sort ( sort ) ; this . update_text ( index , range ) ; brush . move ( this . brush_g [ index ] , range ) ; } else { this . update_text ( index , null ) ; brush . move ( this . brush_g [ index ] , null ) ; } this . brush_g [ index ] . call ( brush ) ; this . reset_handler ( this . brush_g [ index ] ) ; } ) ; } finally { this . ignoreBrushEvents = false ; } } remove ( ) { this . model . off ( 'change:names' , null , this ) ; this . model . off ( 'change:color' , null , this ) ; super . remove ( ) ; } brushes : d3 . BrushBehavior < any > [ ] = [ ] ; brush_g : d3 . Selection < SVGGElement , any , any , any > [ ] = [ ] ; curr_index : number ; selecting_brush : boolean ; names : string [ ] ; }