| <!DOCTYPE html> |
| <!-- |
| Copyright 2016 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| |
| <link rel="import" href="/tracing/base/iteration_helpers.html"> |
| <link rel="import" href="/tracing/base/range.html"> |
| <link rel="import" href="/tracing/base/running_statistics.html"> |
| <link rel="import" href="/tracing/base/sorted_array_utils.html"> |
| <link rel="import" href="/tracing/base/statistics.html"> |
| <link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html"> |
| <link rel="import" href="/tracing/value/unit.html"> |
| |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.v', function() { |
| var Range = tr.b.Range; |
| |
| var MAX_DIAGNOSTIC_MAPS = 16; |
| |
| // p-values less than this indicate statistical significance. |
| var DEFAULT_ALPHA = 0.05; |
| |
| /** @enum */ |
| var Significance = { |
| DONT_CARE: -1, |
| INSIGNIFICANT: 0, |
| SIGNIFICANT: 1 |
| }; |
| |
| function NumericBase(unit) { |
| if (!(unit instanceof tr.v.Unit)) |
| throw new Error('Expected provided unit to be instance of Unit'); |
| |
| this.unit = unit; |
| } |
| |
| NumericBase.prototype = { |
| merge: function(other) { |
| if (this.unit !== other.unit) |
| throw new Error('Merging Numerics with different units'); |
| |
| // Two Numerics that were built using the same NumericBuilder |
| // can be merged using addNumeric(). |
| if (this instanceof Numeric && other instanceof Numeric && |
| this.canAddNumeric(other)) { |
| var result = this.clone(); |
| result.addNumeric(other.clone()); |
| return result; |
| } |
| |
| // Either a Scalar and a Numeric, or two Scalars... |
| // or two Numerics that were not built using the same NumericBuilder, |
| // should be built from their raw samples. |
| var samples = []; |
| this.sampleValuesInto(samples); |
| other.sampleValuesInto(samples); |
| return Numeric.buildFromSamples(this.unit, samples); |
| }, |
| |
| sampleValuesInto: function(samples) { |
| throw new Error('Not implemented'); |
| }, |
| |
| asDict: function() { |
| var d = { |
| unit: this.unit.asJSON() |
| }; |
| |
| this.asDictInto_(d); |
| return d; |
| } |
| }; |
| |
| NumericBase.fromDict = function(d) { |
| if (d.type === 'scalar') |
| return ScalarNumeric.fromDict(d); |
| |
| if (d.type === 'numeric') |
| return Numeric.fromDict(d); |
| |
| throw new Error('Not implemented'); |
| }; |
| |
| function NumericBin(parentNumeric, opt_range) { |
| this.parentNumeric = parentNumeric; |
| this.range = opt_range || (new tr.b.Range()); |
| this.count = 0; |
| this.diagnosticMaps = []; |
| } |
| |
| NumericBin.fromDict = function(parentNumeric, d) { |
| var n = new NumericBin(parentNumeric); |
| n.range.min = d.min; |
| n.range.max = d.max; |
| n.count = d.count; |
| if (d.diagnosticMaps) |
| n.diagnosticMaps = d.diagnosticMaps.map( |
| dmd => tr.v.d.DiagnosticMap.fromDict(dmd)); |
| return n; |
| }; |
| |
| NumericBin.prototype = { |
| /** |
| * @param {*} value |
| * @param {!tr.v.d.DiagnosticMap=} opt_diagnosticMap |
| */ |
| add: function(value, opt_diagnosticMap) { |
| if ((opt_diagnosticMap !== undefined) && |
| !(opt_diagnosticMap instanceof tr.v.d.DiagnosticMap)) |
| throw new Error('Can\'t add a sample with a non-DiagnosticMap'); |
| this.count += 1; |
| if (opt_diagnosticMap) { |
| tr.b.Statistics.uniformlySampleStream( |
| this.diagnosticMaps, this.count, opt_diagnosticMap, |
| MAX_DIAGNOSTIC_MAPS); |
| } |
| }, |
| |
| addBin: function(other) { |
| if (!this.range.equals(other.range)) |
| throw new Error('Merging incompatible Numeric bins.'); |
| tr.b.Statistics.mergeSampledStreams(this.diagnosticMaps, this.count, |
| other.diagnosticMaps, other.count, MAX_DIAGNOSTIC_MAPS); |
| this.count += other.count; |
| }, |
| |
| asDict: function() { |
| return { |
| min: this.range.min, |
| max: this.range.max, |
| count: this.count, |
| diagnosticMaps: this.diagnosticMaps.map(d => d.asDict()) |
| }; |
| }, |
| |
| asJSON: function() { |
| return this.asDict(); |
| } |
| }; |
| |
| function Numeric(unit, range, binInfo) { |
| NumericBase.call(this, unit); |
| |
| this.range = range; |
| |
| this.numNans = 0; |
| this.nanDiagnosticMaps = []; |
| |
| this.running = new tr.b.RunningStatistics(); |
| this.maxCount_ = 0; |
| |
| this.underflowBin = binInfo.underflowBin; |
| this.centralBins = binInfo.centralBins; |
| this.overflowBin = binInfo.overflowBin; |
| |
| this.allBins = []; |
| this.allBins.push(this.underflowBin); |
| this.allBins.push.apply(this.allBins, this.centralBins); |
| this.allBins.push(this.overflowBin); |
| |
| this.allBins.forEach(function(bin) { |
| if (bin.count > this.maxCount_) |
| this.maxCount_ = bin.count; |
| }, this); |
| |
| this.sampleValues_ = []; |
| this.maxNumSampleValues = this.allBins.length * 10; |
| |
| this.summaryOptions = this.defaultSummaryOptions(); |
| } |
| |
| Numeric.fromDict = function(d) { |
| var range = Range.fromExplicitRange(d.min, d.max); |
| var binInfo = {}; |
| binInfo.underflowBin = NumericBin.fromDict(undefined, d.underflowBin); |
| binInfo.centralBins = d.centralBins.map(function(binAsDict) { |
| return NumericBin.fromDict(undefined, binAsDict); |
| }); |
| binInfo.overflowBin = NumericBin.fromDict(undefined, d.overflowBin); |
| var n = new Numeric(tr.v.Unit.fromJSON(d.unit), range, binInfo); |
| n.allBins.forEach(function(bin) { |
| bin.parentNumeric = n; |
| }); |
| if (d.running) |
| n.running = tr.b.RunningStatistics.fromDict(d.running); |
| if (d.summaryOptions) |
| n.customizeSummaryOptions(d.summaryOptions); |
| n.numNans = d.numNans; |
| if (d.nanDiagnosticMaps) { |
| n.nanDiagnosticMaps = d.nanDiagnosticMaps.map( |
| dmd => tr.v.d.DiagnosticMap.fromDict(dmd)); |
| } |
| n.maxNumSampleValues = d.maxNumSampleValues; |
| n.sampleValues_ = d.sampleValues; |
| return n; |
| }; |
| |
| /** |
| * @param {!tr.v.Unit} unit |
| * @param {!Array.<number>} samples |
| * @return {!Numeric} |
| */ |
| Numeric.buildFromSamples = function(unit, samples) { |
| var range = new tr.b.Range(); |
| // Prevent non-numeric samples from introducing NaNs into the range. |
| for (var sample of samples) |
| if (!isNaN(Math.max(sample))) |
| range.addValue(sample); |
| |
| // NumericBuilder.addLinearBins() requires this. |
| if (range.isEmpty) |
| range.addValue(1); |
| if (range.min === range.max) |
| range.addValue(range.min - 1); |
| |
| // This optimizes the resolution when samples are uniformly distributed |
| // (which is almost never the case). |
| var numBins = Math.ceil(Math.sqrt(samples.length)); |
| var builder = new NumericBuilder(unit, range.min); |
| builder.addLinearBins(range.max, numBins); |
| |
| var result = builder.build(); |
| result.maxNumSampleValues = 1000; |
| |
| // TODO(eakuefner): Propagate diagnosticMaps? |
| for (var sample of samples) |
| result.add(sample); |
| |
| return result; |
| }; |
| |
| Numeric.prototype = { |
| __proto__: NumericBase.prototype, |
| |
| get numValues() { |
| return tr.b.Statistics.sum(this.allBins, function(e) { |
| return e.count; |
| }); |
| }, |
| |
| get average() { |
| return this.running.mean; |
| }, |
| |
| get sum() { |
| return this.running.sum; |
| }, |
| |
| get maxCount() { |
| return this.maxCount_; |
| }, |
| |
| /** |
| * Requires that units agree. |
| * Returns DONT_CARE if that is the units' improvementDirection. |
| * Returns SIGNIFICANT if the Mann-Whitney U test returns a |
| * p-value less than opt_alpha or DEFAULT_ALPHA. Returns INSIGNIFICANT if |
| * the p-value is greater than alpha. |
| * |
| * @param {!tr.v.Numeric} other |
| * @param {number=} opt_alpha |
| * @return {!tr.v.Significance} |
| */ |
| getDifferenceSignificance: function(other, opt_alpha) { |
| if (this.unit !== other.unit) |
| throw new Error('Cannot compare Numerics with different units'); |
| |
| if (this.unit.improvementDirection === |
| tr.v.ImprovementDirection.DONT_CARE) { |
| return tr.v.Significance.DONT_CARE; |
| } |
| |
| if (!(other instanceof Numeric)) |
| throw new Error('Unable to compute a p-value'); |
| |
| var mwu = tr.b.Statistics.mwu.test(this.sampleValues, other.sampleValues); |
| if (mwu.p < (opt_alpha || DEFAULT_ALPHA)) |
| return tr.v.Significance.SIGNIFICANT; |
| return tr.v.Significance.INSIGNIFICANT; |
| }, |
| |
| /* |
| * Compute an approximation of percentile based on the counts in the bins. |
| * If the real percentile lies within |this.range| then the result of |
| * the function will deviate from the real percentile by at most |
| * the maximum width of the bin(s) within which the point(s) |
| * from which the real percentile would be calculated lie. |
| * If the real percentile is outside |this.range| then the function |
| * returns the closest range limit: |this.range.min| or |this.range.max|. |
| * |
| * @param {number} percent The percent must be between 0.0 and 1.0. |
| */ |
| getApproximatePercentile: function(percent) { |
| if (!(percent >= 0 && percent <= 1)) |
| throw new Error('percent must be [0,1]'); |
| if (this.numValues == 0) |
| return 0; |
| var valuesToSkip = Math.floor((this.numValues - 1) * percent); |
| for (var i = 0; i < this.allBins.length; i++) { |
| var bin = this.allBins[i]; |
| valuesToSkip -= bin.count; |
| if (valuesToSkip < 0) { |
| if (bin === this.underflowBin) |
| return bin.range.max; |
| else if (bin === this.overflowBin) |
| return bin.range.min; |
| else |
| return bin.range.center; |
| } |
| } |
| throw new Error('Unreachable'); |
| }, |
| |
| getInterpolatedCountAt: function(value) { |
| var bin = this.getBinForValue(value); |
| var idx = this.centralBins.indexOf(bin); |
| if (idx < 0) { |
| // |value| is in either the underflowBin or the overflowBin. |
| // We can't interpolate between infinities. |
| return bin.count; |
| } |
| |
| // |value| must fall between the centers of two bins. |
| // The bin whose center is less than |value| will be this: |
| var lesserBin = bin; |
| |
| // The bin whose center is greater than |value| will be this: |
| var greaterBin = bin; |
| |
| // One of those bins could be an under/overflow bin. |
| // Avoid dealing with Infinities by arbitrarily saying that center of the |
| // underflow bin is its range.max, and the center of the overflow bin is |
| // its range.min. |
| // The centers of bins in |this.centralBins| will default to their |
| // |range.center|. |
| |
| var lesserBinCenter = undefined; |
| var greaterBinCenter = undefined; |
| |
| if (value < greaterBin.range.center) { |
| if (idx > 0) { |
| lesserBin = this.centralBins[idx - 1]; |
| } else { |
| lesserBin = this.underflowBin; |
| lesserBinCenter = lesserBin.range.max; |
| } |
| } else { |
| if (idx < (this.centralBins.length - 1)) { |
| greaterBin = this.centralBins[idx + 1]; |
| } else { |
| greaterBin = this.overflowBin; |
| greaterBinCenter = greaterBin.range.min; |
| } |
| } |
| |
| if (greaterBinCenter === undefined) |
| greaterBinCenter = greaterBin.range.center; |
| |
| if (lesserBinCenter === undefined) |
| lesserBinCenter = lesserBin.range.center; |
| |
| value = tr.b.normalize(value, lesserBinCenter, greaterBinCenter); |
| |
| return tr.b.lerp(value, lesserBin.count, greaterBin.count); |
| }, |
| |
| getBinForValue: function(value) { |
| // Don't use subtraction to avoid arithmetic overflow. |
| var binIndex = tr.b.findHighIndexInSortedArray( |
| this.allBins, b => value < b.range.max ? -1 : 1); |
| return this.allBins[binIndex] || this.overflowBin; |
| }, |
| |
| /** |
| * @param {*} value |
| * @param {!tr.v.d.DiagnosticMap=} opt_diagnosticMap |
| */ |
| add: function(value, opt_diagnosticMap) { |
| if ((opt_diagnosticMap !== undefined) && |
| !(opt_diagnosticMap instanceof tr.v.d.DiagnosticMap)) |
| throw new Error('Can\'t add a sample with a non-DiagnosticMap'); |
| if (typeof(value) !== 'number' || isNaN(value)) { |
| this.numNans++; |
| if (opt_diagnosticMap) { |
| tr.b.Statistics.uniformlySampleStream(this.nanDiagnosticMaps, |
| this.numNans, opt_diagnosticMap, MAX_DIAGNOSTIC_MAPS); |
| } |
| } else { |
| var bin = this.getBinForValue(value); |
| bin.add(value, opt_diagnosticMap); |
| this.running.add(value); |
| if (bin.count > this.maxCount_) |
| this.maxCount_ = bin.count; |
| } |
| |
| tr.b.Statistics.uniformlySampleStream(this.sampleValues_, |
| this.numValues + this.numNans, value, this.maxNumSampleValues); |
| }, |
| |
| sampleValuesInto: function(samples) { |
| for (var sampleValue of this.sampleValues) |
| samples.push(sampleValue); |
| }, |
| |
| /** |
| * Return true if this Numeric can be added to |other|. |
| * |
| * @param {!tr.v.Numeric} other |
| * @return {boolean} |
| */ |
| canAddNumeric: function(other) { |
| if (!this.range.equals(other.range)) |
| return false; |
| if (this.unit !== other.unit) |
| return false; |
| if (this.allBins.length !== other.allBins.length) |
| return false; |
| |
| for (var i = 0; i < this.allBins.length; ++i) |
| if (!this.allBins[i].range.equals(other.allBins[i].range)) |
| return false; |
| |
| return true; |
| }, |
| |
| /** |
| * Add |other| to this Numeric in-place if they can be added. |
| * |
| * @param {!tr.v.Numeric} other |
| */ |
| addNumeric: function(other) { |
| if (!this.canAddNumeric(other)) |
| throw new Error('Merging incompatible Numerics.'); |
| |
| tr.b.Statistics.mergeSampledStreams(this.nanDiagnosticMaps, this.numNans, |
| other.nanDiagnosticMaps, other.numNans, MAX_DIAGNOSTIC_MAPS); |
| tr.b.Statistics.mergeSampledStreams( |
| this.sampleValues, this.numValues, |
| other.sampleValues, other.numValues, tr.b.Statistics.mean( |
| [this.maxNumSampleValues, other.maxNumSampleValues])); |
| this.numNans += other.numNans; |
| this.running = this.running.merge(other.running); |
| for (var i = 0; i < this.allBins.length; ++i) { |
| this.allBins[i].addBin(other.allBins[i]); |
| } |
| }, |
| |
| /** |
| * Controls which statistics are exported to dashboard for this numeric. |
| * The |summaryOptions| parameter is a dictionary with optional boolean |
| * fields |count|, |sum|, |avg|, |std|, |min|, |max| and an optional |
| * array field |percentile|. |
| * Each percentile should be a number between 0.0 and 1.0. |
| * The options not included in the |summaryOptions| will not change. |
| */ |
| customizeSummaryOptions: function(summaryOptions) { |
| tr.b.iterItems(summaryOptions, function(key, value) { |
| this.summaryOptions[key] = value; |
| }, this); |
| }, |
| |
| defaultSummaryOptions: function() { |
| return { |
| count: true, |
| sum: true, |
| avg: true, |
| std: true, |
| min: true, |
| max: true, |
| nans: false, |
| percentile: [] |
| }; |
| }, |
| |
| /** |
| * Returns an array of {name: string, scalar: ScalarNumeric} values. |
| * Each enabled summary option produces the corresponding value: |
| * min, max, count, sum, avg, or std. |
| * Each percentile 0.x produces pct_0x0. |
| * Each percentile 0.xx produces pct_0xx. |
| * Each percentile 0.xxy produces pct_0xx_y. |
| * Percentile 1.0 produces pct_100. |
| */ |
| getSummarizedScalarNumericsWithNames: function() { |
| function statNameToKey(stat) { |
| switch (stat) { |
| case 'std': |
| return 'stddev'; |
| case 'avg': |
| return 'mean'; |
| } |
| return stat; |
| } |
| /** |
| * Converts the given percent to a string in the format specified above. |
| * @param {number} percent The percent must be between 0.0 and 1.0. |
| */ |
| function percentToString(percent) { |
| if (percent < 0 || percent > 1) |
| throw new Error('Percent must be between 0.0 and 1.0'); |
| switch (percent) { |
| case 0: |
| return '000'; |
| case 1: |
| return '100'; |
| } |
| var str = percent.toString(); |
| if (str[1] !== '.') |
| throw new Error('Unexpected percent'); |
| // Pad short strings with zeros. |
| str = str + '0'.repeat(Math.max(4 - str.length, 0)); |
| if (str.length > 4) |
| str = str.slice(0, 4) + '_' + str.slice(4); |
| return '0' + str.slice(2); |
| } |
| |
| var results = []; |
| tr.b.iterItems(this.summaryOptions, function(stat, option) { |
| if (!option) |
| return; |
| if (stat === 'percentile') { |
| option.forEach(function(percent) { |
| var percentile = this.getApproximatePercentile(percent); |
| results.push({ |
| name: 'pct_' + percentToString(percent), |
| scalar: new tr.v.ScalarNumeric(this.unit, percentile) |
| }); |
| }, this); |
| } else if (stat === 'nans') { |
| results.push({ |
| name: 'nans', |
| scalar: new tr.v.ScalarNumeric( |
| tr.v.Unit.byName.count_smallerIsBetter, this.numNans) |
| }); |
| } else { |
| var statUnit = stat === 'count' ? |
| tr.v.Unit.byName.count_smallerIsBetter : this.unit; |
| var key = statNameToKey(stat); |
| var statValue = this.running[key]; |
| if (typeof(statValue) === 'number') { |
| results.push({ |
| name: stat, |
| scalar: new tr.v.ScalarNumeric(statUnit, statValue) |
| }); |
| } |
| } |
| }, this); |
| return results; |
| }, |
| |
| get sampleValues() { |
| return this.sampleValues_; |
| }, |
| |
| clone: function() { |
| return Numeric.fromDict(this.asDict()); |
| }, |
| |
| asDict: function() { |
| var d = { |
| unit: this.unit.asJSON(), |
| type: 'numeric', |
| |
| min: this.range.min, |
| max: this.range.max, |
| |
| numNans: this.numNans, |
| nanDiagnosticMaps: this.nanDiagnosticMaps.map(dm => dm.asDict()), |
| |
| running: this.running.asDict(), |
| summaryOptions: this.summaryOptions, |
| |
| sampleValues: this.sampleValues, |
| maxNumSampleValues: this.maxNumSampleValues, |
| underflowBin: this.underflowBin.asDict(), |
| centralBins: this.centralBins.map(function(bin) { |
| return bin.asDict(); |
| }), |
| overflowBin: this.overflowBin.asDict() |
| }; |
| return d; |
| }, |
| |
| asJSON: function() { |
| return this.asDict(); |
| } |
| }; |
| |
| /** |
| * Reusable builder for tr.v.Numeric objects. |
| * |
| * The bins of the numeric are specified by adding the desired boundaries |
| * between bins. Initially, the builder has only a single boundary: |
| * |
| * minBinBoundary=maxBinBoundary |
| * | |
| * | |
| * -MAX_INT <--------|------------------------------------------> +MAX_INT |
| * : resulting : resulting : |
| * : underflow : overflow : |
| * : bin : bin : |
| * |
| * More boundaries can be added (in increasing order) using addBinBoundary, |
| * addLinearBins and addExponentialBins: |
| * |
| * minBinBoundary maxBinBoundary |
| * | | | | | |
| * | | | | | |
| * -MAX_INT <--------|---------|---------|-----|---------|------> +MAX_INT |
| * : resulting : result. : result. : : result. : resulting : |
| * : underflow : central : central : ... : central : overflow : |
| * : bin : bin 0 : bin 1 : : bin N-1 : bin : |
| * |
| * An important feature of the builder is that it's reusable, i.e. it can be |
| * used to build multiple numerics with the same unit and bin structure. |
| * |
| * @constructor |
| * @param {!tr.v.Unit} unit Unit of the resulting Numeric(s). |
| * @param {number} minBinBoundary The minimum boundary between bins, namely |
| * the underflow bin and the first central bin (or the overflow bin if |
| * no other boundaries are added later). |
| */ |
| function NumericBuilder(unit, minBinBoundary) { |
| this.unit_ = unit; |
| this.boundaries_ = [minBinBoundary]; |
| } |
| |
| NumericBuilder.prototype = { |
| get minBinBoundary() { |
| return this.boundaries_[0]; |
| }, |
| |
| get maxBinBoundary() { |
| return this.boundaries_[this.boundaries_.length - 1]; |
| }, |
| |
| /** |
| * Add a bin boundary |nextMaxBinBoundary| to the builder. |
| * |
| * This operation effectively corresponds to appending a new central bin |
| * with the range [this.maxBinBoundary*, nextMaxBinBoundary]. |
| * |
| * @param {number} nextMaxBinBoundary The added bin boundary (must be |
| * greater than |this.maxMinBoundary|). |
| */ |
| addBinBoundary: function(nextMaxBinBoundary) { |
| if (nextMaxBinBoundary <= this.maxBinBoundary) { |
| throw new Error('The added max bin boundary must be larger than ' + |
| 'the current max boundary'); |
| } |
| this.boundaries_.push(nextMaxBinBoundary); |
| |
| return this; |
| }, |
| |
| /** |
| * Add |binCount| linearly scaled bin boundaries up to |nextMaxBinBoundary| |
| * to the builder. |
| * |
| * This operation corresponds to appending |binCount| central bins of |
| * constant range width |
| * W = ((|nextMaxBinBoundary| - |this.maxBinBoundary|) / |binCount|) |
| * with the following ranges: |
| * |
| * [|this.maxMinBoundary|, |this.maxMinBoundary| + W] |
| * [|this.maxMinBoundary| + W, |this.maxMinBoundary| + 2W] |
| * [|this.maxMinBoundary| + 2W, |this.maxMinBoundary| + 3W] |
| * ... |
| * [|this.maxMinBoundary| + (|binCount| - 2) * W, |
| * |this.maxMinBoundary| + (|binCount| - 2) * W] |
| * [|this.maxMinBoundary| + (|binCount| - 1) * W, |
| * |nextMaxBinBoundary|] |
| * |
| * @param {number} nextBinBoundary The last added bin boundary (must be |
| * greater than |this.maxMinBoundary|). |
| * @param {number} binCount Number of bins to be added (must be positive). |
| */ |
| addLinearBins: function(nextMaxBinBoundary, binCount) { |
| if (binCount <= 0) |
| throw new Error('Bin count must be positive'); |
| |
| var curMaxBinBoundary = this.maxBinBoundary; |
| if (curMaxBinBoundary >= nextMaxBinBoundary) { |
| throw new Error('The new max bin boundary must be greater than ' + |
| 'the previous max bin boundary'); |
| } |
| |
| var binWidth = (nextMaxBinBoundary - curMaxBinBoundary) / binCount; |
| for (var i = 1; i < binCount; i++) |
| this.addBinBoundary(curMaxBinBoundary + i * binWidth); |
| this.addBinBoundary(nextMaxBinBoundary); |
| |
| return this; |
| }, |
| |
| /** |
| * Add |binCount| exponentially scaled bin boundaries up to |
| * |nextMaxBinBoundary| to the builder. |
| * |
| * This operation corresponds to appending |binCount| central bins with |
| * a constant difference between the logarithms of their range min and max |
| * D = ((ln(|nextMaxBinBoundary|) - ln(|this.maxBinBoundary|)) / |binCount|) |
| * with the following ranges: |
| * |
| * [|this.maxMinBoundary|, |this.maxMinBoundary| * exp(D)] |
| * [|this.maxMinBoundary| * exp(D), |this.maxMinBoundary| * exp(2D)] |
| * [|this.maxMinBoundary| * exp(2D), |this.maxMinBoundary| * exp(3D)] |
| * ... |
| * [|this.maxMinBoundary| * exp((|binCount| - 2) * D), |
| * |this.maxMinBoundary| * exp((|binCount| - 2) * D)] |
| * [|this.maxMinBoundary| * exp((|binCount| - 1) * D), |
| * |nextMaxBinBoundary|] |
| * |
| * This method requires that the current max bin boundary is positive. |
| * |
| * @param {number} nextBinBoundary The last added bin boundary (must be |
| * greater than |this.maxMinBoundary|). |
| * @param {number} binCount Number of bins to be added (must be positive). |
| */ |
| addExponentialBins: function(nextMaxBinBoundary, binCount) { |
| if (binCount <= 0) |
| throw new Error('Bin count must be positive'); |
| |
| var curMaxBinBoundary = this.maxBinBoundary; |
| if (curMaxBinBoundary <= 0) |
| throw new Error('Current max bin boundary must be positive'); |
| if (curMaxBinBoundary >= nextMaxBinBoundary) { |
| throw new Error('The last added max boundary must be greater than ' + |
| 'the current max boundary boundary'); |
| } |
| |
| var binExponentWidth = |
| Math.log(nextMaxBinBoundary / curMaxBinBoundary) / binCount; |
| for (var i = 1; i < binCount; i++) { |
| this.addBinBoundary( |
| curMaxBinBoundary * Math.exp(i * binExponentWidth)); |
| } |
| this.addBinBoundary(nextMaxBinBoundary); |
| |
| return this; |
| }, |
| |
| /** |
| * Build a tr.v.Numeric from the list of bin boundaries. |
| * |
| * As explained earlier, this method can be called arbitrarily many times |
| * to produce arbitrarily many distinct numerics. |
| */ |
| build: function() { |
| var binInfo = { |
| underflowBin: new NumericBin(undefined, |
| Range.fromExplicitRange(-Number.MAX_VALUE, this.minBinBoundary)), |
| overflowBin: new NumericBin(undefined, |
| Range.fromExplicitRange(this.maxBinBoundary, Number.MAX_VALUE)), |
| centralBins: new Array(this.boundaries_.length - 1) |
| }; |
| for (var i = 0; i < this.boundaries_.length - 1; i++) { |
| binInfo.centralBins[i] = new NumericBin(undefined, |
| Range.fromExplicitRange( |
| this.boundaries_[i], this.boundaries_[i + 1])); |
| } |
| |
| var numeric = new Numeric( |
| this.unit_, |
| Range.fromExplicitRange(this.minBinBoundary, this.maxBinBoundary), |
| binInfo); |
| numeric.allBins.forEach(function(bin) { |
| bin.parentNumeric = numeric; |
| }); |
| return numeric; |
| } |
| }; |
| |
| /** |
| * Create a linearly scaled tr.v.NumericBuilder with |numBins| bins ranging |
| * from |range.min| to |range.max|. |
| * |
| * @param {!tr.v.Unit} unit |
| * @param {!tr.b.Range} range |
| * @param {number} numBins |
| * @return {tr.v.NumericBuilder} |
| */ |
| NumericBuilder.createLinear = function(unit, range, numBins) { |
| if (range.isEmpty) |
| throw new Error('Range must be non-empty'); |
| return new NumericBuilder(unit, range.min).addLinearBins( |
| range.max, numBins); |
| }; |
| |
| /** |
| * Create an exponentially scaled tr.v.NumericBuilder with |numBins| bins |
| * ranging from |range.min| to |range.max|. |
| * |
| * @param {!tr.v.Unit} unit |
| * @param {!tr.b.Range} range |
| * @param {number} numBins |
| * @return {tr.v.NumericBuilder} |
| */ |
| NumericBuilder.createExponential = function(unit, range, numBins) { |
| if (range.isEmpty) |
| throw new Error('Range must be non-empty'); |
| return new NumericBuilder(unit, range.min).addExponentialBins( |
| range.max, numBins); |
| }; |
| |
| function ScalarNumeric(unit, value) { |
| if (!(unit instanceof tr.v.Unit)) |
| throw new Error('Expected Unit'); |
| |
| if (!(typeof(value) == 'number')) |
| throw new Error('Expected value to be number'); |
| |
| NumericBase.call(this, unit); |
| this.value = value; |
| } |
| |
| ScalarNumeric.prototype = { |
| __proto__: NumericBase.prototype, |
| |
| asDictInto_: function(d) { |
| d.type = 'scalar'; |
| |
| // Infinity and NaN are left out of JSON for security reasons that do not |
| // apply to our use cases. |
| if (this.value === Infinity) |
| d.value = 'Infinity'; |
| else if (this.value === -Infinity) |
| d.value = '-Infinity'; |
| else if (isNaN(this.value)) |
| d.value = 'NaN'; |
| else |
| d.value = this.value; |
| }, |
| |
| sampleValuesInto: function(samples) { |
| samples.push(this.value); |
| }, |
| |
| toString: function() { |
| return this.unit.format(this.value); |
| } |
| }; |
| |
| ScalarNumeric.fromDict = function(d) { |
| // Infinity and NaN are left out of JSON for security reasons that do not |
| // apply to our use cases. |
| if (typeof(d.value) === 'string') { |
| if (d.value === '-Infinity') { |
| d.value = -Infinity; |
| } else if (d.value === 'Infinity') { |
| d.value = Infinity; |
| } else if (d.value === 'NaN') { |
| d.value = NaN; |
| } |
| } |
| |
| return new ScalarNumeric(tr.v.Unit.fromJSON(d.unit), d.value); |
| }; |
| |
| return { |
| Significance: Significance, |
| NumericBase: NumericBase, |
| NumericBin: NumericBin, |
| Numeric: Numeric, |
| NumericBuilder: NumericBuilder, |
| ScalarNumeric: ScalarNumeric |
| }; |
| }); |
| </script> |