Skip to content

Commit c22d00c

Browse files
authored
line chart: implement ignoreOutlier (#4405)
This change back ports some logic from vz-chart-helpers to ignore y dimension outliers when computing the default viewBox.
1 parent 74aeb44 commit c22d00c

File tree

6 files changed

+199
-40
lines changed

6 files changed

+199
-40
lines changed

tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
[seriesData]="dataSeries"
9090
[seriesMetadataMap]="chartMetadataMap"
9191
[yScaleType]="newYAxisType"
92+
[ignoreYOutliers]="ignoreOutliers"
9293
></line-chart>
9394

9495
<ng-template #legacyChart>

tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class TestableGpuLineChart {
8888
@Input() seriesData!: DataSeries[];
8989
@Input() seriesMetadataMap!: DataSeriesMetadataMap;
9090
@Input() yScaleType!: ScaleType;
91+
@Input() ignoreYOutliers: boolean = false;
9192
}
9293

9394
describe('scalar card', () => {

tensorboard/webapp/widgets/line_chart_v2/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ tf_ts_library(
4343
],
4444
visibility = ["//visibility:private"],
4545
deps = [
46+
"//tensorboard/webapp/third_party:d3",
4647
"//tensorboard/webapp/widgets/line_chart_v2/lib:public_types",
4748
"//tensorboard/webapp/widgets/line_chart_v2/lib:utils",
4849
],

tensorboard/webapp/widgets/line_chart_v2/line_chart_component.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
100100
@Input()
101101
tooltipTemplate?: TooltipTemplate;
102102

103+
/**
104+
* Whether to ignore outlier when computing default viewBox from the dataSeries.
105+
*
106+
* Do note that we only take values in between approxmiately 5th to 95th percentiles.
107+
*/
108+
@Input()
109+
ignoreYOutliers: boolean = false;
110+
103111
readonly Y_GRID_COUNT = 6;
104112
readonly X_GRID_COUNT = 10;
105113

@@ -174,13 +182,15 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
174182
* Returns true when default view box changes (e.g., due to more data coming in
175183
* or more series becoming visible).
176184
*
177-
* Calculating the dataExtent and updating the viewBox accordingly can be expensive as
178-
* (1) we have to iterate over ~1M data points and (2) supporting `ignoreOutlier` will
179-
* make us remember 5th and 95th percentile presumably using min and max heaps
180-
* (increased memory usage).
185+
* Calculating the dataExtent and updating the viewBox accordingly can be an expensive
186+
* operation.
181187
*/
182188
private shouldUpdateDefaultViewBox(changes: SimpleChanges): boolean {
183-
if (changes['xScaleType'] || changes['yScaleType']) {
189+
if (
190+
changes['xScaleType'] ||
191+
changes['yScaleType'] ||
192+
changes['ignoreYOutliers']
193+
) {
184194
return true;
185195
}
186196

@@ -313,7 +323,8 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
313323
} else if (!this.isViewBoxOverriden && this.isViewBoxChanged) {
314324
const dataExtent = computeDataSeriesExtent(
315325
this.seriesData,
316-
this.seriesMetadataMap
326+
this.seriesMetadataMap,
327+
this.ignoreYOutliers
317328
);
318329
this.viewBox = {
319330
x: this.xScale.niceDomain(dataExtent.x ?? DEFAULT_EXTENT.x),

tensorboard/webapp/widgets/line_chart_v2/line_chart_internal_utils.ts

+27-20
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
See the License for the specific language governing permissions and
1313
limitations under the License.
1414
==============================================================================*/
15+
import * as d3 from '../../third_party/d3';
1516
import {
1617
DataSeries,
1718
DataSeriesMetadataMap,
@@ -22,44 +23,50 @@ import {isWebGl2Supported} from './lib/utils';
2223
/**
2324
* Returns extent, min and max values of each dimensions, of all data series points.
2425
*
25-
* Note that it excludes auxillary data points and invisible data series.
26+
* When ignoreYOutliers is true, it will calculate extent using values within 5th and 95th
27+
* quantiles.
2628
*
27-
* TODO(stephanwlee): add support for ignoreOutlier.
29+
* Note that it excludes auxillary data points and invisible data series.
2830
*/
2931
export function computeDataSeriesExtent(
3032
data: DataSeries[],
31-
metadataMap: DataSeriesMetadataMap
33+
metadataMap: DataSeriesMetadataMap,
34+
ignoreYOutliers: boolean
3235
): {x: [number, number] | undefined; y: [number, number] | undefined} {
33-
let xMin = Infinity;
34-
let xMax = -Infinity;
35-
let yMin = Infinity;
36-
let yMax = -Infinity;
37-
38-
let xExtentChanged = false;
39-
let yExtentChanged = false;
36+
let xMin: number | null = null;
37+
let xMax: number | null = null;
38+
let yPoints: number[] = [];
4039

40+
let pointIndex = 0;
4141
for (const {id, points} of data) {
4242
const meta = metadataMap[id];
4343
if (!meta || meta.aux || !meta.visible) continue;
4444

4545
for (let index = 0; index < points.length; index++) {
4646
const {x, y} = points[index];
47-
if (!Number.isNaN(x)) {
48-
xMin = Math.min(xMin, x);
49-
xMax = Math.max(xMax, x);
50-
xExtentChanged = true;
47+
if (Number.isFinite(x)) {
48+
xMin = xMin === null || x < xMin ? x : xMin;
49+
xMax = xMax === null || x > xMax ? x : xMax;
5150
}
52-
if (!Number.isNaN(y)) {
53-
yMin = Math.min(yMin, y);
54-
yMax = Math.max(yMax, y);
55-
yExtentChanged = true;
51+
if (Number.isFinite(y)) {
52+
yPoints.push(y);
5653
}
54+
pointIndex++;
5755
}
5856
}
5957

58+
yPoints.sort(d3.ascending);
59+
let yMin = yPoints[0];
60+
let yMax = yPoints[yPoints.length - 1];
61+
62+
if (ignoreYOutliers && yPoints.length > 2) {
63+
yMin = yPoints[Math.ceil((yPoints.length - 1) * 0.05)];
64+
yMax = yPoints[Math.floor((yPoints.length - 1) * 0.95)];
65+
}
66+
6067
return {
61-
x: xExtentChanged ? [xMin, xMax] : undefined,
62-
y: yExtentChanged ? [yMin, yMax] : undefined,
68+
x: xMin !== null && xMax !== null ? [xMin, xMax] : undefined,
69+
y: yMin !== undefined && yMax !== undefined ? [yMin, yMax] : undefined,
6370
};
6471
}
6572

0 commit comments

Comments
 (0)