提交 dfb37b9b 编写于 作者: A A. Unique TensorFlower 提交者: TensorFlower Gardener

Adding histogram chart component

Change: 119381434
上级 ec1e6c94
......@@ -44,13 +44,23 @@ module TF.Backend {
export interface Histogram {
min: number;
max: number;
nItems: number;
sum: number;
sumSquares: number;
nItems?: number;
sum?: number;
sumSquares?: number;
bucketRightEdges: number[];
bucketCounts: number[];
}
export interface HistogramBin {
x: number,
dx: number,
y: number
}
export type HistogramSeriesDatum = HistogramSeries & Datum;
export interface HistogramSeries {
bins: HistogramBin[]
}
export type ImageDatum = Datum & Image
export interface Image {
width: number;
......@@ -169,11 +179,20 @@ module TF.Backend {
/**
* Return a promise containing HistogramDatums for given run and tag.
*/
public histogram(tag: string, run: string): Promise<Array<HistogramDatum>> {
public histogram(tag: string, run: string): Promise<Array<HistogramSeriesDatum>> {
let p: Promise<TupleData<HistogramTuple>[]>;
let url = this.router.histograms(tag, run);
p = this.requestManager.request(url);
return p.then(map(detupler(createHistogram)));
return p.then(map(detupler(createHistogram)))
.then(function(histos) {
return histos.map(function(histo, i) {
return {
wall_time: histo.wall_time,
step: histo.step,
bins: convertBins(histo)
};
});
});
}
/**
......@@ -277,6 +296,43 @@ module TF.Backend {
};
};
/**
* Takes histogram data as stored by tensorboard backend and converts it to
* the standard d3 histogram data format to make it more compatible and easier to
* visualize. When visualizing histograms, having the left edge and width makes
* things quite a bit easier.
*
* @param {histogram} Histogram - A histogram from tensorboard backend.
* @return {HistogramBin[]} - Each bin has an x (left edge), a dx (width), and a y (count).
*
* If given rightedges are inclusive, then these left edges (x) are exclusive.
*/
export function convertBins (histogram: Histogram) {
if (histogram.bucketRightEdges.length !== histogram.bucketCounts.length) {
throw(new Error("Edges and counts are of different lengths."))
}
var previousRightEdge = histogram.min;
return histogram.bucketRightEdges.map(function(rightEdge: number, i: number) {
// Use the previous bin's rightEdge as the new leftEdge
var left = previousRightEdge;
// We need to clip the rightEdge because right-most edge can be
// infinite-sized
var right = Math.min(histogram.max, rightEdge);
// Store rightEdgeValue for next iteration
previousRightEdge = rightEdge;
return {
x: left,
dx: right - left,
y: histogram.bucketCounts[i]
};
});
}
// The following interfaces (TupleData, HistogramTuple, CompressedHistogramTuple,
// and ImageMetadata) describe how the data is sent over from the backend, and thus
// wall_time, step, value
......
......@@ -78,13 +78,7 @@ module TF.Backend {
backend.histogram("histo1", "run1").then((histos) => {
var histo = histos[0];
assertIsDatum(histo);
assert.isNumber(histo.min);
assert.isNumber(histo.max);
assert.isNumber(histo.sum);
assert.isNumber(histo.sumSquares);
assert.isNumber(histo.nItems);
assert.instanceOf(histo.bucketRightEdges, Array);
assert.instanceOf(histo.bucketRightEdges, Array);
assert.instanceOf(histo.bins, Array);
done();
});
});
......@@ -156,4 +150,71 @@ module TF.Backend {
assert.deepEqual(getTags(empty2), []);
});
});
describe("Verify that the histogram format conversion works.", function() {
function assertHistogramEquality(h1, h2) {
h1.forEach(function(b1, i) {
var b2 = h2[i];
assert.closeTo(b1.x, b2.x, 1e-10);
assert.closeTo(b1.dx, b2.dx, 1e-10);
assert.closeTo(b1.y, b2.y, 1e-10);
});
}
it("Throws and error if the inputs are of different lengths", function() {
assert.throws(function() {
convertBins({bucketRightEdges:[0], bucketCounts:[1, 2], min: 1, max: 2});
}, "Edges and counts are of different lengths.")
});
it("Handles data with no bins", function() {
assert.deepEqual(convertBins({bucketRightEdges: [], bucketCounts: [], min: 0, max: 0}), []);
});
it("Handles data with one bin", function() {
var counts = [1];
var rightEdges = [1.21e-12];
var histogram = [
{ x: 1.1e-12, dx: 1.21e-12 - 1.1e-12, y: 1 }
];
var newHistogram = convertBins({bucketRightEdges: rightEdges, bucketCounts: counts, min: 1.1e-12, max: 1.21e-12});
assertHistogramEquality(newHistogram, histogram);
});
it("Handles data with two bins.", function() {
var counts = [1, 2];
var rightEdges = [1.1e-12, 1.21e-12];
var histogram = [
{ x: 1.0e-12, dx: 1.1e-12 - 1.0e-12, y: 1 },
{ x: 1.1e-12, dx: 1.21e-12 - 1.1e-12, y: 2 }
];
var newHistogram = convertBins({bucketRightEdges: rightEdges, bucketCounts: counts, min: 1.0e-12, max: 1.21e-12});
assertHistogramEquality(newHistogram, histogram);
});
it("Handles a domain that crosses zero, but doesn't include zero as an edge.", function() {
var counts = [1, 2];
var rightEdges = [-1.0e-12, 1.0e-12];
var histogram = [
{ x: -1.1e-12, dx: 1.1e-12 - 1.0e-12, y: 1 },
{ x: -1.0e-12, dx: 2.0e-12, y: 2 }
];
var newHistogram = convertBins({bucketRightEdges: rightEdges, bucketCounts: counts, min: -1.1e-12, max: 1.0e-12});
assertHistogramEquality(newHistogram, histogram);
});
it("Handles a right-most right edge that extends to very large number.", function() {
var counts = [1, 2, 3];
var rightEdges = [0, 1.0e-12, 1.0e14];
var histogram = [
{ x: -1.0e-12, dx: 1.0e-12, y: 1 },
{ x: 0, dx: 1.0e-12, y: 2 },
{ x: 1.0e-12, dx: 1.1e-12 - 1.0e-12, y: 3 }
];
var newHistogram = convertBins({bucketRightEdges: rightEdges, bucketCounts: counts, min: -1.0e-12, max: 1.1e-12});
assertHistogramEquality(newHistogram, histogram);
});
});
}
module TF.Histogram {
/**
* Re-bins histogram data into uniform-width bins. Assumes a uniform distribution of values in given bins.
*
* @param {HistogramBin[]} bins - The original histogram data,
* @param {number} numberOfBins - The number of uniform-width bins to split the data into.
* @return {HistogramBin[]} - Re-binned histogram data. Does not modify original data, returns a new array.
*/
export function rebinHistogram(bins: TF.Backend.HistogramBin[], numberOfBins: number) {
if (bins.length === 0) return [];
var oldBinsXExtent = [
d3.min(bins, function(old:any) { return old.x; }),
d3.max(bins, function(old:any) { return old.x + old.dx; })
];
var newDx: number = (oldBinsXExtent[1] - oldBinsXExtent[0]) / numberOfBins;
var newBins: TF.Backend.HistogramBin[] = d3.range(oldBinsXExtent[0], oldBinsXExtent[1], newDx).map(function(newX) {
// Take the count of each existing bin, multiply it by the proportion of
// overlap with the new bin, then sum and store as the count for new
// bin. If no overlap, will add zero, if 100% overlap, will include full
// count into new bin.
var newY = d3.sum(bins.map(function(old) {
var intersectDx = Math.min(old.x + old.dx, newX + newDx) - Math.max(old.x, newX);
return (intersectDx > 0) ? (intersectDx / old.dx) * old.y : 0;
}));
return {
x: newX,
dx: newDx,
y: newY
};
});
return newBins;
}
}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="../../web-component-tester/browser.js"></script>
<script src="../../webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="../../tf-imports/d3.html">
</head>
<body>
<script src="../rebin.js"></script>
<script src="rebinTests.js"></script>
</body>
</html>
/* Copyright 2015 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
module TF.Histogram {
let assert = chai.assert;
describe("Rebin", function() {
var assertHistogramEquality = function(h1, h2) {
h1.forEach(function(b1, i) {
var b2 = h2[i];
assert.closeTo(b1.x, b2.x, 1e-10);
assert.closeTo(b1.dx, b2.dx, 1e-10);
assert.closeTo(b1.y, b2.y, 1e-10);
});
}
//
// Rebinning
//
it("Returns an empty array if you don't have any bins", function() {
assert.deepEqual(rebinHistogram([], 10), []);
});
it("Collapses two bins into one.", function() {
var histogram = [
{x: 0, dx: 1, y: 1},
{x: 1, dx: 1, y: 2}
];
var oneBin = [
{x: 0, dx: 2, y: 3}
];
assertHistogramEquality(rebinHistogram(histogram, 1), oneBin);
});
it("Splits one bin into two.", function() {
var histogram = [
{x: 0, dx: 1, y: 3}
];
var twoBin = [
{x: 0, dx: 0.5, y: 1.5},
{x: 0.5, dx: 0.5, y: 1.5}
];
assertHistogramEquality(rebinHistogram(histogram, 2), twoBin);
});
it("Regularizes non-uniform bins.", function() {
var histogram = [
{x: 0, dx: 2, y: 3},
{x: 2, dx: 3, y: 3},
{x: 5, dx: 1, y: 1}
];
var twoBin = [
{x: 0, dx: 3, y: 4},
{x: 3, dx: 3, y: 3}
];
assertHistogramEquality(rebinHistogram(histogram, 2), twoBin);
});
});
}
<link rel="import" href="../polymer/polymer.html">
<dom-module id="tf-vz-histogram-series">
<template>
<style>
:host {
display: block;
}
svg {
font-family: roboto, sans-serif;
}
.background {
fill-opacity: 0;
fill: red;
}
.histogram {
pointer-events: none;
}
.hover {
font-size: 9px;
dominant-baseline: middle;
opacity: 0;
}
.hover circle {
stroke: white;
stroke-opacity: 0.5;
stroke-width: 1px;
}
.baseline {
stroke: black;
stroke-opacity: 0.1;
}
.outline {
fill: none;
stroke: white;
stroke-opacity: 0.5;
}
.x-axis-hover {
pointer-events: none;
}
.x-axis-hover .label {
opacity: 0;
font-weight: bold;
font-size: 11px;
text-anchor: end;
}
.x-axis-hover text {
text-anchor: middle;
}
.x-axis-hover line {
stroke: black;
}
.x-axis-hover rect {
fill: white;
}
.gradient {
}
.axis {
font-size: 10px;
fill: #aaa;
}
.axis path.domain {
fill: none;
}
.axis .tick line {
stroke: #ddd;
}
.axis.slice {
opacity: 0;
}
.axis.slice .tick line {
stroke-dasharray: 2;
}
.small .axis text { display: none; }
.small .axis .tick:first-of-type text { display: block; }
.small .axis .tick:last-of-type text { display: block; }
.medium .axis text { display: none; }
.medium .axis .tick:nth-child(2n + 1) text { display: block; }
.large .axis text { display: none; }
.large .axis .tick:nth-child(2n + 1) text { display: block; }
</style>
<svg id="svg">
<g>
<g class="axis x"></g>
<g class="axis y"></g>
<g class="axis y slice"></g>
<g class="stage">
<rect class="background"></rect>
</g>
<g class="x-axis-hover"></g>
</g>
</svg>
</template>
<script>
"use strict";
Polymer({
is: "tf-vz-histogram-series",
properties: {
mode: { type: String, value: "offset" }, //offset | overlay
width: { type: Number, value: 500 },
height: { type: Number, value: 500 },
timeProperty: { type: String, value: "step" },
bins: { type: String, value: "bins" },
x: { type: String, value: "x" },
dx: { type: String, value: "dx" },
y: { type: String, value: "y" },
data: { type: Array, value: function(){ return [{ step: 0, bins: [{ x: 0, dx: 1, y: 0 }] }, { step: 1, bins: [{ x: 0, dx: 1, y: 0 }] }];}}
// type: HistogramSeriesDatum[] as described in tf-vz-histogram-series.d.ts
},
ready: function() {
// Polymer's way of scoping styles on nodes that d3 created
this.scopeSubtree(this.$["svg"], true);
},
draw: function(duration) {
//
// Data verification
//
if (!(this.data.length > 0)) throw(new Error("Not enough steps in the data"));
if (!this.data[0].hasOwnProperty(this.timeProperty)) throw(new Error("No time property of '" + this.timeProperty + "' in data"));
if (!this.data[0].hasOwnProperty(this.bins)) throw(new Error("No bins property of '" + this.bins + "' in data"));
if (!(this.data[0][this.bins].length > 0)) throw(new Error("Must have at least one bin in bins in data"));
if (!this.data[0][this.bins][0].hasOwnProperty(this.x)) throw(new Error("No x property '" + this.x + "' on bins data"));
if (!this.data[0][this.bins][0].hasOwnProperty(this.dx)) throw(new Error("No dx property '" + this.dx + "' on bins data"));
if (!this.data[0][this.bins][0].hasOwnProperty(this.y)) throw(new Error("No y property '" + this.y + "' on bins data"));
//
// Initialization
//
var timeProp = this.timeProperty;
var xProp = this.x;
var binsProp = this.bins;
var dxProp = this.dx;
var yProp = this.y;
var xAccessor = (d) => d[xProp];
var yAccessor = (d) => d[yProp];
var dxAccessor = (d) => d[dxProp];
var xRightAccessor = (d) => d[xProp] + d[dxProp];
var timeAccessor = (d) => d[timeProp];
var duration = duration | 0;
var data = this.data;
var mode = this.mode;
var outerWidth = this.width,
outerHeight = this.height;
var sliceHeight,
margin = {top: 5, right: 60, bottom: 20, left: 24};
if (mode === "offset") {
sliceHeight = outerHeight / 2.5;
margin.top = sliceHeight + 5;
} else {
sliceHeight = outerHeight - margin.top - margin.bottom;
}
var width = outerWidth - margin.left - margin.right,
height = outerHeight - margin.top - margin.bottom;
var leftMin = d3.min(data, xAccessor),
rightMax = d3.max(data, xRightAccessor);
//
// Text formatters
//
var formatTime = d3.time.format("%x"),
format = d3.format(".3n");
//
// Calculate the extents
//
var xExtents = data.map(function(d, i) {
return [
d3.min(d[binsProp], xAccessor),
d3.max(d[binsProp], xRightAccessor)
];
});
var yExtents = data.map(function(d) {
return d3.extent(d[binsProp], yAccessor);
});
//
// Scales and axis
//
var outlineCanvasSize = 500;
var yScale = (timeProp === "step" ? d3.scale.linear() : d3.time.scale())
.domain(d3.extent(data, timeAccessor))
.range([0, (mode === "offset" ? height : 0)]);
var ySliceScale = d3.scale.linear()
.domain([0, d3.max(data, function(d, i) { return yExtents[i][1]; })])
.range([sliceHeight, 0]);
var yLineScale = d3.scale.linear()
.domain(ySliceScale.domain())
.range([outlineCanvasSize, 0]);
var xScale = d3.scale.linear()
.domain([
d3.min(data, function(d, i) { return xExtents[i][0]; }),
d3.max(data, function(d, i) { return xExtents[i][1]; })
])
.nice()
.range([0, width]);
var xLineScale = d3.scale.linear()
.domain(xScale.domain())
.range([0, outlineCanvasSize]);
var outlineColor = d3.scale.linear()
.domain(d3.extent(data, timeAccessor))
.range(["#FFA726", "#BF360C"])
.interpolate(d3.interpolateHcl);
var xAxis = d3.svg.axis()
.scale(xScale)
.ticks(Math.max(2, width / 20))
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.ticks(Math.max(2, width / 20))
.orient("right");
var ySliceAxis = d3.svg.axis()
.scale(ySliceScale)
.ticks(Math.max(2, width / 20))
.tickSize(width + 5)
.orient("right");
var path = d3.svg.area()
.interpolate("linear")
.x(function(d) { return xLineScale(d[xProp] + d[dxProp] / 2); })
.y0(function(d) { return yLineScale(0); })
.y1(function(d) { return yLineScale(d[yProp]); });
//
// Render
//
var svgNode = this.$.svg;
var svg = d3.select(svgNode)
var svgTransition = svg.transition().duration(duration)
.attr("width", outerWidth)
.attr("height", outerHeight);
var g = svg.select("g")
.classed("small", function() { return (width > 0 && width <= 150); })
.classed("medium", function() { return (width > 150 && width <= 300); })
.classed("large", function() { return (width > 300); })
var gTransition = svgTransition.select("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var bisect = d3.bisector(xRightAccessor).left;
var stage = g.select(".stage")
.on("mouseover", function() {
hoverUpdate.transition().style("opacity", 1);
edgeLabelUpdate.transition().style("opacity", 1)
})
.on("mouseout", function() {
hoverUpdate.transition().style("opacity", 0);
edgeLabelUpdate.transition().style("opacity", 0)
})
.on("mousemove", function() {
var m = d3.mouse(this),
v = xScale.invert(m[0]);
function hoverXIndex(d) {
return Math.min(d[binsProp].length - 1, bisect(d[binsProp], v));
}
var lastSliceData;
hoverUpdate
.attr("transform", function(d) {
var index = hoverXIndex(d);
lastSliceData = d;
return "translate(" + xScale(d[binsProp][index][xProp] + d[binsProp][index][dxProp] / 2) + "," + ySliceScale(d[binsProp][index][yProp]) + ")";
});
var index = hoverXIndex(lastSliceData);
edgeLabelUpdate
.attr("transform", function(d) { return"translate(" + xScale(lastSliceData[binsProp][index][xProp] + lastSliceData[binsProp][index][dxProp] / 2) + ", " + height + ")"; })
.select("text")
.text(function(d) { return format(lastSliceData[binsProp][index][xProp] + lastSliceData[binsProp][index][dxProp] / 2); });
});
var background = stage.select(".background")
.attr("transform", "translate(" + -margin.left + "," + -margin.top + ")")
.attr("width", outerWidth)
.attr("height", outerHeight);
var histogram = stage.selectAll(".histogram").data(data, function(d) { return d[timeProp]; }),
histogramExit = histogram.exit().remove(),
histogramEnter = histogram.enter().append("g").attr("class", "histogram"),
histogramUpdate = histogram
.sort(function(a, b) { return a[timeProp] - b[timeProp]; }),
histogramTransition = gTransition.selectAll(".histogram")
.attr("transform", function(d) {
return "translate(0, " + (mode === "offset" ? (yScale(d[timeProp]) - sliceHeight) : 0) + ")";
});
var baselineEnter = histogramEnter.append("line").attr("class", "baseline"),
baselineUpdate = histogramTransition.select(".baseline")
.style("stroke-opacity", function(d) { return (mode === "offset" ? 0.1 : 0); })
.attr("y1", sliceHeight)
.attr("y2", sliceHeight)
.attr("x2", width);
var outlineEnter = histogramEnter.append("path").attr("class", "outline"),
outlineUpdate = histogramUpdate.select(".outline")
.attr("vector-effect", "non-scaling-stroke")
.attr("d", function(d) { return path(d[binsProp]); })
.style("stroke-width", 1),
outlineTransition = histogramTransition.select(".outline")
.attr("transform", "scale(" + width / outlineCanvasSize + ", " + sliceHeight / outlineCanvasSize + ")")
.style("stroke", function(d) { return (mode === "offset" ? "white" : outlineColor(d[timeProp])); })
.style("fill-opacity", function(d) { return (mode === "offset" ? 1 : 0); })
.style("fill", function(d) { return outlineColor(d[timeProp]); });
var hoverEnter = histogramEnter.append("g")
.attr("class", "hover")
.style("fill", function(d) { return outlineColor(d[timeProp]); }),
hoverUpdate = histogramUpdate.select(".hover");
hoverEnter.append("circle")
.attr("r", 2);
hoverEnter.append("text")
.style("display", "none")
.style("stroke", "white")
.style("stroke-width", 2)
.attr("dx", 4);
hoverEnter.append("text")
.style("display", "none")
.attr("dx", 4);
var edgeLabel = g.select(".x-axis-hover").selectAll(".label").data(["x"]),
edgeLabelEnter = edgeLabel.enter().append("g").attr("class", "label"),
edgeLabelUpdate = edgeLabel;
edgeLabelEnter.append("rect")
.attr("x", -20)
.attr("y", 6)
.attr("width", 40)
.attr("height", 14)
edgeLabelEnter.append("line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", 0)
.attr("y2", 6);
edgeLabelEnter.append("text")
.attr("dy", 18);
gTransition.select(".y.axis.slice")
.style("opacity", mode === "offset" ? 0 : 1)
.attr("transform", "translate(0, " + (mode === "offset" ? -sliceHeight : 0) + ")")
.call(ySliceAxis);
gTransition.select(".x.axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis);
gTransition.select(".y.axis")
.style("opacity", mode === "offset" ? 1 : 0)
.attr("transform", "translate(" + width + ", " + (mode === "offset" ? 0 : height) + ")")
.call(yAxis);
}
});
</script>
</dom-module>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册