|  | @@ -307,7 +307,7 @@ func (analyser *BurndownAnalysis) MergeResults(
 | 
	
		
			
				|  |  |  	r1, r2 interface{}, c1, c2 *CommonAnalysisResult) interface{} {
 | 
	
		
			
				|  |  |  	bar1 := r1.(BurndownResult)
 | 
	
		
			
				|  |  |  	bar2 := r2.(BurndownResult)
 | 
	
		
			
				|  |  | -  merged := BurndownResult{}
 | 
	
		
			
				|  |  | +	merged := BurndownResult{}
 | 
	
		
			
				|  |  |  	if bar1.sampling < bar2.sampling {
 | 
	
		
			
				|  |  |  		merged.sampling = bar1.sampling
 | 
	
		
			
				|  |  |  	} else {
 | 
	
	
		
			
				|  | @@ -318,9 +318,20 @@ func (analyser *BurndownAnalysis) MergeResults(
 | 
	
		
			
				|  |  |  	} else {
 | 
	
		
			
				|  |  |  		merged.granularity = bar2.granularity
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	people := make([]string, len(bar1.reversedPeopleDict))
 | 
	
		
			
				|  |  | -	copy(people, bar1.reversedPeopleDict)
 | 
	
		
			
				|  |  | -	merged.reversedPeopleDict = append(people, bar2.reversedPeopleDict...)
 | 
	
		
			
				|  |  | +	people := map[string]int{}
 | 
	
		
			
				|  |  | +	for _, id := range bar1.reversedPeopleDict {
 | 
	
		
			
				|  |  | +		people[id] = len(people)
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	for _, id := range bar2.reversedPeopleDict {
 | 
	
		
			
				|  |  | +		if _, exists := people[id]; !exists {
 | 
	
		
			
				|  |  | +			people[id] = len(people)
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	merged.reversedPeopleDict = make([]string, len(people))
 | 
	
		
			
				|  |  | +	for name, index := range people {
 | 
	
		
			
				|  |  | +		merged.reversedPeopleDict[index] = name
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  	// interpolate to daily and sum
 | 
	
		
			
				|  |  |  	_ = bar1
 | 
	
		
			
				|  |  |  	_ = bar2
 | 
	
	
		
			
				|  | @@ -328,8 +339,68 @@ func (analyser *BurndownAnalysis) MergeResults(
 | 
	
		
			
				|  |  |  	// return merged
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -func interpolateMatrix(matrix [][]int64, granularity, sampling int, daily [][]int64, offset int) {
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +func mergeMatrices(m1, m2 [][]int64, granularity1, sampling1, granularity2, sampling2 int,
 | 
	
		
			
				|  |  | +	c1, c2 *CommonAnalysisResult) {
 | 
	
		
			
				|  |  | +	commonMerged := CommonAnalysisResult{}
 | 
	
		
			
				|  |  | +	commonMerged.Merge(c1)
 | 
	
		
			
				|  |  | +	commonMerged.Merge(c2)
 | 
	
		
			
				|  |  | +	size := (commonMerged.EndTime - commonMerged.BeginTime) / (3600 * 24)
 | 
	
		
			
				|  |  | +	daily := make([][]float32, size)
 | 
	
		
			
				|  |  | +	for i := range daily {
 | 
	
		
			
				|  |  | +		daily[i] = make([]float32, size)
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	addMatrix(m1, granularity1, sampling1, daily,
 | 
	
		
			
				|  |  | +		int(c1.BeginTime-commonMerged.BeginTime)/(3600*24))
 | 
	
		
			
				|  |  | +	addMatrix(m2, granularity2, sampling2, daily,
 | 
	
		
			
				|  |  | +		int(c2.BeginTime-commonMerged.BeginTime)/(3600*24))
 | 
	
		
			
				|  |  | +	// convert daily to [][]int64
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func addMatrix(matrix [][]int64, granularity, sampling int, daily [][]float32, offset int) {
 | 
	
		
			
				|  |  | +	/*
 | 
	
		
			
				|  |  | +	 daily_matrix = numpy.zeros(
 | 
	
		
			
				|  |  | +	            (matrix.shape[0] * granularity, matrix.shape[1] * sampling),
 | 
	
		
			
				|  |  | +	            dtype=numpy.float32)
 | 
	
		
			
				|  |  | +	*/
 | 
	
		
			
				|  |  | +	// Determine the maximum number of bands; the actual one may be larger but we do not care
 | 
	
		
			
				|  |  | +	maxCols := 0
 | 
	
		
			
				|  |  | +	for _, row := range matrix {
 | 
	
		
			
				|  |  | +		if maxCols < len(row) {
 | 
	
		
			
				|  |  | +			maxCols = len(row)
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	// Ported from labours.py load_burndown()
 | 
	
		
			
				|  |  | +	for y := 0; y < maxCols; y++ {
 | 
	
		
			
				|  |  | +		for x := 0; x < len(matrix); x++ {
 | 
	
		
			
				|  |  | +			if (y+1)*granularity <= x*sampling {
 | 
	
		
			
				|  |  | +				// interpolate
 | 
	
		
			
				|  |  | +				var previous int64
 | 
	
		
			
				|  |  | +				if x > 0 && y < len(matrix[x-1]) {
 | 
	
		
			
				|  |  | +					previous = matrix[x-1][y]
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +				for i := 0; i < sampling; i++ {
 | 
	
		
			
				|  |  | +					var value float32
 | 
	
		
			
				|  |  | +					if y < len(matrix[x]) {
 | 
	
		
			
				|  |  | +						value = (float32(previous) +
 | 
	
		
			
				|  |  | +							float32((matrix[x][y]-previous)*int64(i))/float32(sampling)) / float32(granularity)
 | 
	
		
			
				|  |  | +					} else {
 | 
	
		
			
				|  |  | +						value = float32(previous) *
 | 
	
		
			
				|  |  | +							(float32(1) - float32(i)/float32(sampling)) / float32(granularity)
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +					for j := y * granularity; j < (y+1)*granularity; j++ {
 | 
	
		
			
				|  |  | +						daily[j+offset][x*sampling+i+offset] += value
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +			} else if y*granularity <= (x+1)*sampling && y < len(matrix[x]) {
 | 
	
		
			
				|  |  | +				// fill constant
 | 
	
		
			
				|  |  | +				for suby := y*granularity + offset; suby < (y+1)*granularity+offset; suby++ {
 | 
	
		
			
				|  |  | +					for subx := suby; subx < (x+1)*sampling+offset; subx++ {
 | 
	
		
			
				|  |  | +						daily[suby][subx] += float32(matrix[x][y]) / float32(granularity)
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (analyser *BurndownAnalysis) serializeText(result *BurndownResult, writer io.Writer) {
 |