@ -1,10 +1,12 @@
'use strict' ;
/*global*/
import { bar , bb , bubble , Chart , ChartOptions , DataItem , selection , zoom } from 'billboard.js' ;
import { bar , bb , bubble , Chart , ChartOptions , ChartTypes , DataItem , zoom } from 'billboard.js' ;
// import BubbleCompare from 'billboard.js/dist/plugin/billboardjs-plugin-bubblecompare';
// import { scaleSqrt } from 'd3-scale';
import { Commit , State } from '../../../../premium/webviews/timeline/protocol' ;
import { formatDate , fromNow } from '../../shared/date' ;
import { Emitter , Event } from '../../shared/events' ;
import { throttle } from '../../shared/utils' ;
export interface DataPointClickEvent {
data : {
@ -19,27 +21,244 @@ export class TimelineChart {
return this . _onDidClickDataPoint . event ;
}
private readonly _chart : Chart ;
private _commitsByTimestamp = new Map < string , Commit > ( ) ;
private _authorsByIndex = new Map < number , string > ( ) ;
private _indexByAuthors = new Map < string , number > ( ) ;
private readonly $container : HTMLElement ;
private _chart : Chart | undefined ;
private _chartDimensions : { height : number ; width : number } ;
private readonly _resizeObserver : ResizeObserver ;
private readonly _selector : string ;
private readonly _commitsByTimestamp = new Map < string , Commit > ( ) ;
private readonly _authorsByIndex = new Map < number , string > ( ) ;
private readonly _indexByAuthors = new Map < string , number > ( ) ;
private _dateFormat : string = undefined ! ;
constructor ( selector : string ) {
this . _selector = selector ;
const fn = throttle ( ( ) = > {
const dimensions = this . _chartDimensions ;
this . _chart ? . resize ( {
width : dimensions.width ,
height : dimensions.height - 10 ,
} ) ;
} , 100 ) ;
this . _resizeObserver = new ResizeObserver ( entries = > {
const size = entries [ 0 ] . borderBoxSize [ 0 ] ;
const dimensions = {
width : Math.floor ( size . inlineSize ) ,
height : Math.floor ( size . blockSize ) ,
} ;
if (
this . _chartDimensions . height === dimensions . height &&
this . _chartDimensions . width === dimensions . width
) {
return ;
}
this . _chartDimensions = dimensions ;
fn ( ) ;
} ) ;
this . $container = document . querySelector ( selector ) ! . parentElement ! ;
const rect = this . $container . getBoundingClientRect ( ) ;
this . _chartDimensions = { height : Math.floor ( rect . height ) , width : Math.floor ( rect . width ) } ;
this . _resizeObserver . observe ( this . $container ) ;
}
reset() {
this . _chart ? . unselect ( ) ;
this . _chart ? . unzoom ( ) ;
}
updateChart ( state : State ) {
this . _dateFormat = state . dateFormat ;
this . _commitsByTimestamp . clear ( ) ;
this . _authorsByIndex . clear ( ) ;
this . _indexByAuthors . clear ( ) ;
if ( state ? . dataset == null || state . dataset . length === 0 ) {
this . _chart ? . destroy ( ) ;
this . _chart = undefined ;
const $overlay = document . getElementById ( 'chart-empty-overlay' ) as HTMLDivElement ;
$overlay ? . classList . toggle ( 'hidden' , false ) ;
const $emptyMessage = $overlay . querySelector ( '[data-bind="empty"]' ) as HTMLHeadingElement ;
$emptyMessage . textContent = state . title ;
return ;
}
const $overlay = document . getElementById ( 'chart-empty-overlay' ) as HTMLDivElement ;
$overlay ? . classList . toggle ( 'hidden' , true ) ;
const xs : { [ key : string ] : string } = { } ;
const colors : { [ key : string ] : string } = { } ;
const names : { [ key : string ] : string } = { } ;
const axes : { [ key : string ] : string } = { } ;
const types : { [ key : string ] : ChartTypes } = { } ;
const groups : string [ ] [ ] = [ ] ;
const series : { [ key : string ] : any } = { } ;
const group = [ ] ;
let index = 0 ;
let commit : Commit ;
let author : string ;
let date : string ;
let additions : number ;
let deletions : number ;
// // Get the min and max additions and deletions from the dataset
// let minChanges = Infinity;
// let maxChanges = -Infinity;
// for (const commit of state.dataset) {
// const changes = commit.additions + commit.deletions;
// if (changes < minChanges) {
// minChanges = changes;
// }
// if (changes > maxChanges) {
// maxChanges = changes;
// }
// }
// const bubbleScale = scaleSqrt([minChanges, maxChanges], [6, 100]);
for ( commit of state . dataset ) {
( { author , date , additions , deletions } = commit ) ;
if ( ! this . _indexByAuthors . has ( author ) ) {
this . _indexByAuthors . set ( author , index ) ;
this . _authorsByIndex . set ( index , author ) ;
index -- ;
}
const x = 'time' ;
if ( series [ x ] == null ) {
series [ x ] = [ ] ;
series [ 'additions' ] = [ ] ;
series [ 'deletions' ] = [ ] ;
xs [ 'additions' ] = x ;
xs [ 'deletions' ] = x ;
axes [ 'additions' ] = 'y2' ;
axes [ 'deletions' ] = 'y2' ;
names [ 'additions' ] = 'Additions' ;
names [ 'deletions' ] = 'Deletions' ;
colors [ 'additions' ] = 'rgba(73, 190, 71, 1)' ;
colors [ 'deletions' ] = 'rgba(195, 32, 45, 1)' ;
types [ 'additions' ] = bar ( ) ;
types [ 'deletions' ] = bar ( ) ;
group . push ( x ) ;
groups . push ( [ 'additions' , 'deletions' ] ) ;
}
const authorX = ` ${ x } . ${ author } ` ;
if ( series [ authorX ] == null ) {
series [ authorX ] = [ ] ;
series [ author ] = [ ] ;
xs [ author ] = authorX ;
axes [ author ] = 'y' ;
names [ author ] = author ;
types [ author ] = bubble ( ) ;
group . push ( author , authorX ) ;
}
series [ x ] . push ( date ) ;
series [ 'additions' ] . push ( additions ) ;
series [ 'deletions' ] . push ( deletions ) ;
series [ authorX ] . push ( date ) ;
const z = additions + deletions ; //bubbleScale(additions + deletions);
series [ author ] . push ( {
y : this._indexByAuthors.get ( author ) ,
z : z ,
} ) ;
this . _commitsByTimestamp . set ( date , commit ) ;
}
groups . push ( group ) ;
const columns = Object . entries ( series ) . map ( ( [ key , value ] ) = > [ key , . . . value ] ) ;
if ( this . _chart == null ) {
const options = this . getChartOptions ( ) ;
if ( options . axis == null ) {
options . axis = { y : { tick : { } } } ;
}
if ( options . axis . y == null ) {
options . axis . y = { tick : { } } ;
}
if ( options . axis . y . tick == null ) {
options . axis . y . tick = { } ;
}
options . axis . y . min = index - 2 ;
options . axis . y . tick . values = [ . . . this . _authorsByIndex . keys ( ) ] ;
options . data = {
. . . options . data ,
axes : axes ,
colors : colors ,
columns : columns ,
groups : groups ,
names : names ,
types : types ,
xs : xs ,
} ;
this . _chart = bb . generate ( options ) ;
} else {
this . _chart . config ( 'axis.y.tick.values' , [ . . . this . _authorsByIndex . keys ( ) ] , false ) ;
this . _chart . config ( 'axis.y.min' , index - 2 , false ) ;
this . _chart . groups ( groups ) ;
this . _chart . load ( {
axes : axes ,
colors : colors ,
columns : columns ,
names : names ,
types : types ,
xs : xs ,
unload : true ,
} ) ;
}
}
private getChartOptions() {
const config : ChartOptions = {
bindto : selector ,
bindto : this._ selector,
data : {
columns : [ ] ,
types : { time : bubble ( ) , additions : bar ( ) , deletions : bar ( ) } ,
xFormat : '%Y-%m-%dT%H:%M:%S.%LZ' ,
xLocaltime : false ,
selection : {
enabled : selection ( ) ,
draggable : false ,
grouped : false ,
multiple : false ,
} ,
// selection: {
// enabled: selection(),
// draggable: false,
// grouped: false,
// multiple: false,
// },
onclick : this.onDataPointClicked.bind ( this ) ,
} ,
axis : {
@ -61,7 +280,7 @@ export class TimelineChart {
y : {
max : 0 ,
padding : {
top : 50 ,
top : 7 5,
bottom : 100 ,
} ,
show : true ,
@ -72,16 +291,16 @@ export class TimelineChart {
} ,
y2 : {
label : {
text : 'Number of Lines C hanged' ,
text : 'Lines c hanged' ,
position : 'outer-middle' ,
} ,
// min: 0,
show : true ,
tick : {
outer : true ,
// culling: true,
// stepSize: 1,
} ,
// tick: {
// outer: true,
// // culling: true,
// // stepSize: 1,
// },
} ,
} ,
bar : {
@ -90,7 +309,8 @@ export class TimelineChart {
padding : 2 ,
} ,
bubble : {
maxR : 50 ,
maxR : 100 ,
zerobased : true ,
} ,
grid : {
focus : {
@ -98,7 +318,7 @@ export class TimelineChart {
show : true ,
y : true ,
} ,
front : tru e,
front : fals e,
lines : {
front : false ,
} ,
@ -113,20 +333,12 @@ export class TimelineChart {
show : true ,
padding : 10 ,
} ,
// point: {
// r: 6,
// focus: {
// expand: {
// enabled: true,
// r: 9,
// },
// },
// select: {
// r: 12,
// },
// },
resize : {
auto : true ,
auto : false ,
} ,
size : {
height : this._chartDimensions.height - 10 ,
width : this._chartDimensions.width ,
} ,
tooltip : {
grouped : true ,
@ -160,148 +372,20 @@ export class TimelineChart {
} ,
// plugins: [
// new BubbleCompare({
// minR: 3 ,
// maxR: 3 0,
// minR: 6 ,
// maxR: 10 0,
// expandScale: 1.2,
// }),
// ],
} ;
this . _chart = bb . generate ( config ) ;
}
private onDataPointClicked ( d : DataItem , _element : SVGElement ) {
const commit = this . _commitsByTimestamp . get ( new Date ( d . x ) . toISOString ( ) ) ;
if ( commit == null ) return ;
const selected = this . _chart . selected ( d . id ) as unknown as DataItem [ ] ;
this . _onDidClickDataPoint . fire ( {
data : {
id : commit.commit ,
selected : selected?. [ 0 ] ? . id === d . id ,
} ,
} ) ;
}
reset() {
this . _chart . unselect ( ) ;
this . _chart . unzoom ( ) ;
}
updateChart ( state : State ) {
this . _dateFormat = state . dateFormat ;
this . _commitsByTimestamp . clear ( ) ;
this . _authorsByIndex . clear ( ) ;
this . _indexByAuthors . clear ( ) ;
if ( state ? . dataset == null ) {
this . _chart . unload ( ) ;
return ;
}
const xs : { [ key : string ] : string } = { } ;
const colors : { [ key : string ] : string } = { } ;
const names : { [ key : string ] : string } = { } ;
const axes : { [ key : string ] : string } = { } ;
const types : { [ key : string ] : string } = { } ;
const groups : string [ ] [ ] = [ ] ;
const series : { [ key : string ] : any } = { } ;
const group = [ ] ;
let index = 0 ;
let commit : Commit ;
let author : string ;
let date : string ;
let additions : number ;
let deletions : number ;
for ( commit of state . dataset ) {
( { author , date , additions , deletions } = commit ) ;
if ( ! this . _indexByAuthors . has ( author ) ) {
this . _indexByAuthors . set ( author , index ) ;
this . _authorsByIndex . set ( index , author ) ;
index -- ;
}
const x = 'time' ;
if ( series [ x ] == null ) {
series [ x ] = [ ] ;
series [ 'additions' ] = [ ] ;
series [ 'deletions' ] = [ ] ;
xs [ 'additions' ] = x ;
xs [ 'deletions' ] = x ;
axes [ 'additions' ] = 'y2' ;
axes [ 'deletions' ] = 'y2' ;
names [ 'additions' ] = 'Additions' ;
names [ 'deletions' ] = 'Deletions' ;
colors [ 'additions' ] = 'rgba(73, 190, 71, 1)' ;
colors [ 'deletions' ] = 'rgba(195, 32, 45, 1)' ;
types [ 'additions' ] = bar ( ) ;
types [ 'deletions' ] = bar ( ) ;
group . push ( x ) ;
groups . push ( [ 'additions' , 'deletions' ] ) ;
}
const authorX = ` ${ x } . ${ author } ` ;
if ( series [ authorX ] == null ) {
series [ authorX ] = [ ] ;
series [ author ] = [ ] ;
xs [ author ] = authorX ;
axes [ author ] = 'y' ;
names [ author ] = author ;
types [ author ] = bubble ( ) ;
group . push ( author , authorX ) ;
}
series [ x ] . push ( date ) ;
series [ 'additions' ] . push ( additions ) ;
series [ 'deletions' ] . push ( deletions ) ;
series [ authorX ] . push ( date ) ;
series [ author ] . push ( { /*x: date,*/ y : this._indexByAuthors.get ( author ) , z : additions + deletions } ) ;
this . _commitsByTimestamp . set ( date , commit ) ;
}
this . _chart . config ( 'axis.y.tick.values' , [ . . . this . _authorsByIndex . keys ( ) ] , false ) ;
this . _chart . config ( 'axis.y.min' , index - 2 , false ) ;
groups . push ( group ) ;
this . _chart . groups ( groups ) ;
const columns = Object . entries ( series ) . map ( ( [ key , value ] ) = > [ key , . . . value ] ) ;
this . _chart . load ( {
columns : columns ,
xs : xs ,
axes : axes ,
names : names ,
colors : colors ,
types : types ,
unload : true ,
} ) ;
return config ;
}
private getTooltipName ( name : string , ratio : number , id : string , index : number ) {
if ( id === 'additions' || /*id === 'changes' ||*/ id === 'deletions' ) return name ;
const date = new Date ( this . _chart . data ( id ) [ 0 ] . values [ index ] . x ) ;
const date = new Date ( this . _chart ! . data ( id ) [ 0 ] . values [ index ] . x ) ;
const commit = this . _commitsByTimestamp . get ( date . toISOString ( ) ) ;
return commit ? . commit . slice ( 0 , 8 ) ? ? '00000000' ;
}
@ -320,10 +404,23 @@ export class TimelineChart {
return value === 0 ? undefined ! : value ;
}
const date = new Date ( this . _chart . data ( id ) [ 0 ] . values [ index ] . x ) ;
const date = new Date ( this . _chart ! . data ( id ) [ 0 ] . values [ index ] . x ) ;
const commit = this . _commitsByTimestamp . get ( date . toISOString ( ) ) ;
return commit ? . message ? ? '???' ;
}
private onDataPointClicked ( d : DataItem , _element : SVGElement ) {
const commit = this . _commitsByTimestamp . get ( new Date ( d . x ) . toISOString ( ) ) ;
if ( commit == null ) return ;
// const selected = this._chart!.selected(d.id) as unknown as DataItem[];
this . _onDidClickDataPoint . fire ( {
data : {
id : commit.commit ,
selected : true , //selected?.[0]?.id === d.id,
} ,
} ) ;
}
}
function capitalize ( s : string ) : string {