Skip to content

Commit

Permalink
Merge pull request #54 from palantir/dev
Browse files Browse the repository at this point in the history
Merge version 0.1.0 into master - the demo day release
teamdandelion committed Feb 4, 2014
2 parents ab292cf + 86dfac7 commit 6851aba
Showing 30 changed files with 1,233 additions and 105 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -3,3 +3,5 @@ plottable.js
plottable.js.map
tests.js
tests.js.map
examples/*.js
examples/*.js.map
6 changes: 4 additions & 2 deletions Gruntfile.js
Original file line number Diff line number Diff line change
@@ -23,7 +23,8 @@ module.exports = function(grunt) {
},
files: [
{ src: ["src/*.ts"], dest: "plottable.js" },
{ src: ["test/*.ts"], dest: "test/tests.js" }
{ src: ["test/*.ts"], dest: "test/tests.js" },
{ src: ["examples/*.ts"], dest: "examples/examples.js"}
]
}
},
@@ -35,7 +36,8 @@ module.exports = function(grunt) {
"files": [
"Gruntfile.js",
"src/*.ts",
"test/*.ts"
"test/*.ts",
"examples/*.ts"
]
}
}
34 changes: 34 additions & 0 deletions examples/demo-day-crazy.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.demo-table-title text {
text-decoration: line-through;
fill: yellow;
font-family: Times;
}

.scatterplot-title text {
font-size: 24pt;
fill: magenta;
font-family: monospace;
}

.histogram-title text {
font-size: 24pt;
fill: cyan;
font-family: cursive;
}

rect {
fill: green;
}

circle {
fill: orange;
}

.selected-point {
fill: black;
}

.drag-box {
fill: red;
opacity: 0.5;
}
20 changes: 20 additions & 0 deletions examples/demo-day-crazy.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<link href="../style.css" type="text/css" rel="stylesheet" />
<link href="demo-day-crazy.css" type="text/css" rel="stylesheet" />
</head>
<body>
<br><hr><br>
<svg id="table" width="800px" height="600px"></svg>
<br><hr><br>


<script type="text/javascript"> window.demoName = "demo-day"; //HACK HACK </script>
<script src="../Lib/chai/chai.js"></script>
<script src="../Lib/lodash.js"></script>
<script src="../Lib/d3.ascii.js"></script>
<script src="../plottable.js"></script>
<script src="examples.js"></script>

</body>
14 changes: 14 additions & 0 deletions examples/demo-day.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.demo-table-title text {
text-decoration: underline;
}

.scatterplot-title text {
font-size: 24pt;
}

.histogram-title text {
font-size: 24pt;

.axis-label text {
font-size: 12pt;
}
20 changes: 20 additions & 0 deletions examples/demo-day.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<link href="../style.css" type="text/css" rel="stylesheet" />
<link href="demo-day.css" type="text/css" rel="stylesheet" />
</head>
<body>
<br><hr><br>
<svg id="table" width="800px" height="600px"></svg>
<br><hr><br>


<script type="text/javascript"> window.demoName = "demo-day"; //HACK HACK </script>
<script src="../Lib/chai/chai.js"></script>
<script src="../Lib/lodash.js"></script>
<script src="../Lib/d3.ascii.js"></script>
<script src="../plottable.js"></script>
<script src="examples.js"></script>

</body>
93 changes: 64 additions & 29 deletions src/demo.ts → examples/demo.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
///<reference path="../lib/d3.d.ts" />
///<reference path="../lib/chai/chai.d.ts" />
///<reference path="../src/interfaces.d.ts" />

///<reference path="../src/table.ts" />
///<reference path="../src/renderer.ts" />
///<reference path="../src/interaction.ts" />
///<reference path="../src/labelComponent.ts" />
///<reference path="../src/axis.ts" />
///<reference path="exampleUtil.ts" />

///<reference path="table.ts" />
///<reference path="renderer.ts" />
///<reference path="interaction.ts" />

function makeRandomData(numPoints, scaleFactor=1): IDataset {
var data = [];
for (var i = 0; i < numPoints; i++) {
var x = Math.random();
var r = {x: x, y: (x + x * Math.random()) * scaleFactor}
data.push(r);
}
data = _.sortBy(data, (d) => d.x);
return {"data": data, "seriesName": "random-data"};
}


if ((<any> window).demoName == "demo1") {
// make a regular table with 1 axis on bottom, 1 axis on left, renderer in center

var svg1 = d3.select("#svg1");
@@ -31,7 +26,7 @@ var basicTable = new Table([[renderArea, yAxis], [xAxis, null]])
basicTable.anchor(svg1);
basicTable.computeLayout();
basicTable.render();
new DragZoomInteraction(renderArea.hitBox, [xAxis, yAxis, renderArea], xScale, yScale);
new PanZoomInteraction(renderArea, [xAxis, yAxis, renderArea], xScale, yScale);



@@ -55,6 +50,8 @@ var t3 = makeBasicChartTable();
var t4 = makeBasicChartTable();

var metaTable = new Table([[t1, t2], [t3, t4]]);
metaTable.rowPadding = 5;
metaTable.colPadding = 5;
metaTable.anchor(svg2);
svg2.attr("width", 800).attr("height", 600);
metaTable.computeLayout();
@@ -72,7 +69,6 @@ function makeMultiAxisChart() {
var data = makeRandomData(30);
var renderArea = new LineRenderer(data, xScale, yScale);
var rootTable = new Table([[renderArea, rightAxesTable], [xAxis, null]])
console.log(rootTable);
return rootTable;

}
@@ -99,20 +95,22 @@ function makeSparklineMultichart() {
var row1: Component[] = [leftAxesTable, renderer1, rightAxesTable];
var yScale2 = new LinearScale();
var leftAxis = new YAxis(yScale2, "left");
var data2 = makeRandomData(100, 100000);
leftAxis.xAlignment = "RIGHT";
var data2 = makeRandomData(1000, 100000);
var renderer2 = new CircleRenderer(data2, xScale1, yScale2);
var toggleClass = function() {return !d3.select(this).classed("selected-point")};
var cb = (s) => s.classed("selected-point", toggleClass);
var areaInteraction = new AreaInteraction(renderer2, null, cb);
var row2: Component[] = [leftAxis, renderer2, null];
var bottomAxis = new XAxis(xScale1, "bottom");
var row3: Component[] = [null, bottomAxis, null];
var xScaleSpark = new LinearScale();
var yScaleSpark = new LinearScale();
var sparkline = new LineRenderer(data2, xScale1, yScaleSpark);
var sparkline = new LineRenderer(data2, xScaleSpark, yScaleSpark);
sparkline.rowWeight(0.25);
var row4 = [null, sparkline, null];
var zoomInteraction = new BrushZoomInteraction(sparkline, xScale1, yScale2);
var multiChart = new Table([row1, row2, row3, row4]);
// multiChart.xMargin = 0;
// multiChart.yMargin = 0;
// multiChart.xPadding = 0;
// multiChart.yPadding = 0;
return multiChart;
}

@@ -134,10 +132,47 @@ multichart.render();
// var bottomAxes = iterate(bottom, () => new xAxis(yScale, "bottom"))
// }

function iterate(n: number, fn: () => any) {
var out = [];
for (var i=0; i<n; i++) {
out.push(fn())
}
return out;
}
var svg5 = d3.select("#svg5");
svg5.attr("width", 500).attr("height", 500);
var xScale = new LinearScale();
var yScale = new LinearScale();
var xAxis = new XAxis(xScale, "bottom");

var yAxisRight = new YAxis(yScale, "right");
var yAxisRightLabel = new AxisLabel("bp y right qd", "vertical-right");
var yAxisRightTable = new Table([[yAxisRight, yAxisRightLabel]]);
yAxisRightTable.colWeight(0);

var yAxisLeft = new YAxis(yScale, "left");
var yAxisLeftLabel = new AxisLabel("bp y left qd", "vertical-left");
var yAxisLeftTable = new Table([[yAxisLeftLabel, yAxisLeft]]);
yAxisLeftTable.colWeight(0);

var data = makeRandomData(30);
var renderArea = new LineRenderer(data, xScale, yScale);
var basicTable = new Table([[yAxisLeftTable, renderArea, yAxisRightTable], [null, xAxis, null]]);
var title = new TitleLabel("bpIqd");
var outerTable = new Table([[title], [basicTable]]);
outerTable.anchor(svg5);
outerTable.computeLayout();
outerTable.render();


// bar renderer test
var svg6 = d3.select("#svg6");
svg6.attr("width", 500).attr("height", 500);
var xScale = new LinearScale();
var yScale = new LinearScale();
var xAxis = new XAxis(xScale, "bottom");
var yAxis = new YAxis(yScale, "left");

var bucketData = makeRandomBucketData(10, 10, 80);

var replacementData = makeRandomBucketData(5, 20, 80);

var BarRenderArea = new BarRenderer(bucketData, xScale, yScale);
var basicTable = new Table([[yAxis, BarRenderArea], [null, xAxis]])
basicTable.anchor(svg6);
basicTable.computeLayout();
basicTable.render();
} // hackhack
30 changes: 30 additions & 0 deletions examples/demo1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<link href="../style.css" type="text/css" rel="stylesheet" />
</head>
<body>

<h1> Bar Renderer </h1>
<svg id="svg6"></svg><br><hr><br>


<h1> Basic TSC </h1>
<svg id="svg1"></svg><br><hr><br>
<h1> Chartbag of timeseriescharts </h1>
<svg id="svg2"></svg><br><hr><br>
<h1> TSC with 2 axes </h1>
<svg id="svg3"></svg><br><hr><br>
<h1> TSC with subplots, varying # of axes, and sparkline </h1>
<svg id="svg4"></svg><br><hr><br>
<svg id="svg5"></svg><br><hr><br>


<script src="../Lib/d3.ascii.js"></script>
<script src="../Lib/lodash.js"></script>
<script src="../Lib/chai/chai.js"></script>
<script type="text/javascript"> window.demoName = "demo1"; //HACK HACK </script>
<script src="../plottable.js"></script>
<script src="examples.js"></script>

</body>
140 changes: 140 additions & 0 deletions examples/demoDay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
///<reference path="../lib/d3.d.ts" />

///<reference path="../src/table.ts" />
///<reference path="../src/renderer.ts" />
///<reference path="../src/interaction.ts" />
///<reference path="../src/labelComponent.ts" />
///<reference path="../src/axis.ts" />
///<reference path="../src/scale.ts" />
///<reference path="exampleUtil.ts" />

if ((<any> window).demoName === "demo-day") {
var N_BINS = 25;
function makeScatterPlotWithSparkline(data) {
var s: any = {};
s.xScale = new LinearScale();
s.yScale = new LinearScale();
s.leftAxis = new YAxis(s.yScale, "left");
var leftAxisTable = new Table([[new AxisLabel("y", "vertical-left"), s.leftAxis]]);
leftAxisTable.colWeight(0);
s.xAxis = new XAxis(s.xScale, "bottom");
var xAxisTable = new Table([[s.xAxis], [new AxisLabel("x")]]);
xAxisTable.rowWeight(0);

s.renderer = new CircleRenderer(data, s.xScale, s.yScale, null, null, 1.5);
s.xSpark = new LinearScale();
s.ySpark = new LinearScale();
s.sparkline = new CircleRenderer(data, s.xSpark, s.ySpark, null, null, 0.5);
s.sparkline.rowWeight(0.25);
var r1 = [leftAxisTable, s.renderer];
var r2 = [null, xAxisTable];
var r3 = [null, s.sparkline];
s.table = new Table([r1,r2,r3]);
return s;
}

function makeHistograms(data: any[]) {
var h: any = {};
var xExtent = d3.extent(data, (d) => d.x);
h.xScale1 = new LinearScale().domain(xExtent);
h.yScale1 = new LinearScale();
h.bin1 = makeBinFunction((d) => d.x, xExtent, N_BINS);
var data1 = h.bin1(data);
var ds1 = {data: data1, seriesName: "xVals"}
h.renderer1 = new BarRenderer(ds1, h.xScale1, h.yScale1);
h.xAxis1 = new XAxis(h.xScale1, "bottom");
h.yAxis1 = new YAxis(h.yScale1, "right");
var labelX1Table = new Table([[h.xAxis1], [new AxisLabel("X values")]]);
labelX1Table.rowWeight(0);
var labelY1Table = new Table([[h.yAxis1, new AxisLabel("Counts", "vertical-right")]]);
labelY1Table.colWeight(0);
var table1 = new Table([[h.renderer1, labelY1Table], [labelX1Table, null]]);

var yExtent = d3.extent(data, (d) => d.y);
h.xScale2 = new LinearScale().domain(yExtent);
h.yScale2 = new LinearScale();
h.bin2 = makeBinFunction((d) => d.y, yExtent, N_BINS);
var data2 = h.bin2(data);
var ds2 = {data: data2, seriesName: "yVals"}
h.renderer2 = new BarRenderer(ds2, h.xScale2, h.yScale2);
h.xAxis2 = new XAxis(h.xScale2, "bottom");
h.yAxis2 = new YAxis(h.yScale2, "right");
var labelX2Table = new Table([[h.xAxis2], [new AxisLabel("Y values")]]);
labelX2Table.rowWeight(0);
var labelY2Table = new Table([[h.yAxis2, new AxisLabel("Counts", "vertical-right")]]);
labelY2Table.colWeight(0);
var table2 = new Table([[h.renderer2, labelY2Table], [labelX2Table, null]]);

h.table = new Table([[table1], [table2]]);
h.table.rowPadding = 5;
h.table.colPadding = 5;
return h;
}

function makeScatterHisto(data) {
var s = makeScatterPlotWithSparkline(data);
var h = makeHistograms(data.data);
var r = [s.table, h.table];
var titleRow = [ new TitleLabel("Random Data").classed("scatterplot-title", true),
new TitleLabel("Histograms").classed("histogram-title", true) ];
var chartTable = new Table([titleRow, r]);
chartTable.colPadding = 10;
var table = new Table([[new TitleLabel("Glorious Demo Day Demo of Glory").classed("demo-table-title", true)], [chartTable]]);

return {table: table, s: s, h: h};
}

function coordinator(chart: any, dataset: IDataset) {
var scatterplot = chart.s;
var histogram = chart.h;
chart.c = {};

var lastSelection = null;
var selectionCallback = (selection: D3.Selection) => {
if (lastSelection != null) lastSelection.classed("selected-point", false);
selection.classed("selected-point", true);
lastSelection = selection;
}

var data = dataset.data;
// var lastSelectedData = null;
var dataCallback = (selectedIndices: number[]) => {
var selectedData = grabIndices(data, selectedIndices);
// selectedData.forEach((d) => d.selected = true);
// if (lastSelectedData != null) lastSelectedData.forEach((d) => d.selected = false);
// lastSelectedData = selectedData;
var xBins = histogram.bin1(selectedData);
var yBins = histogram.bin2(selectedData);
chart.c.xBins = xBins;
chart.c.yBins = yBins;
histogram.renderer1.data({seriesName: "xBins", data: xBins})
histogram.renderer2.data({seriesName: "yBins", data: yBins})
histogram.renderer1.render();
histogram.renderer2.render();
};
var areaInteraction = new AreaInteraction(scatterplot.renderer, null, selectionCallback, dataCallback);
var zoomCallback = (indices) => {areaInteraction.clearBox(); dataCallback(indices)};
chart.c.zoom = new BrushZoomInteraction(scatterplot.sparkline, scatterplot.xScale, scatterplot.yScale, zoomCallback);
}

function grabIndices(itemsToGrab: any[], indices: number[]) {
return indices.map((i) => itemsToGrab[i]);
}
var clump1 = makeNormallyDistributedData(300, -10, 5, 7, 1);
var clump2 = makeNormallyDistributedData(300, 2, 0.5, 3, 3);
var clump3 = makeNormallyDistributedData(30, 5, 10, -3, 9);
var clump4 = makeNormallyDistributedData(200, -25, 1, 20, 5);

var clumpData = clump1.concat(clump2, clump3, clump4);
var dataset = {seriesName: "clumpedData", data: clumpData};

var chartSH = makeScatterHisto(dataset);

coordinator(chartSH, dataset);

var svg = d3.select("#table");
chartSH.table.anchor(svg);
chartSH.table.computeLayout();
chartSH.table.render();

}
69 changes: 69 additions & 0 deletions examples/exampleUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
function makeRandomData(numPoints, scaleFactor=1): IDataset {
var data = [];
for (var i = 0; i < numPoints; i++) {
var x = Math.random();
var r = {x: x, y: (x + x * Math.random()) * scaleFactor}
data.push(r);
}
data = _.sortBy(data, (d) => d.x);
return {"data": data, "seriesName": "random-data"};
}

function makeNormallyDistributedData(n=100, xMean?, xStdDev?, yMean?, yStdDev?) {
var results = [];
var x = d3.random.normal(xMean, xStdDev);
var y = d3.random.normal(yMean, yStdDev);
for (var i=0; i<n; i++) {
var r = {x: x(), y: y()};
results.push(r);
}
return results;
}
function makeBinFunction(accessor, range, nBins) {
return (d) => binByVal(d, accessor, range, nBins);
}

function binByVal(data: any[], accessor: IAccessor, range=[0,100], nBins=10) {
if (accessor == null) {accessor = (d) => d.x};
var min = range[0];
var max = range[1];
var spread = max-min;
var binBeginnings = _.range(nBins).map((n) => min + n * spread / nBins);
var binEndings = _.range(nBins) .map((n) => min + (n+1) * spread / nBins);
var counts = new Array(nBins);
_.range(nBins).forEach((b, i) => counts[i] = 0);
data.forEach((d) => {
var v = accessor(d);
var found = false;
for (var i=0; i<nBins; i++) {
if (v <= binEndings[i]) {
counts[i]++;
found = true;
break;
}
}
if (!found) {counts[counts.length-1]++};
});
var bins = counts.map((count, i) => {
var bin: any = {};
bin.x = binBeginnings[i];
bin.x2 = binEndings[i];
bin.y = count;
return bin;
})
return bins;
}
function makeRandomBucketData(numBuckets: number, bucketWidth: number, maxValue = 10): IDataset {
var data = [];
for (var i=0; i < numBuckets; i++) {
data.push({
x: i * bucketWidth,
x2: (i+1) * bucketWidth,
y: Math.round(Math.random() * maxValue)
});
}
return {
"data": data,
"seriesName": "random-buckets"
};
}
18 changes: 18 additions & 0 deletions examples/sparkline-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<link href="../style.css" type="text/css" rel="stylesheet" />
</head>
<body>
<h1> Basic TSC </h1>
<svg id="table" width="800px" height="600px"></svg><br><hr><br>


<script type="text/javascript"> window.demoName = "sparkline-demo"; //HACK HACK </script>
<script src="../Lib/chai/chai.js"></script>
<script src="../Lib/lodash.js"></script>
<script src="../Lib/d3.ascii.js"></script>
<script src="../plottable.js"></script>
<script src="examples.js"></script>

</body>
43 changes: 43 additions & 0 deletions examples/sparklineDemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
///<reference path="../lib/d3.d.ts" />

///<reference path="../src/table.ts" />
///<reference path="../src/renderer.ts" />
///<reference path="../src/interaction.ts" />
///<reference path="../src/labelComponent.ts" />
///<reference path="../src/axis.ts" />
///<reference path="../src/scale.ts" />
///<reference path="exampleUtil.ts" />

if ((<any> window).demoName === "sparkline-demo") {

var yScale = new LinearScale();
var xScale = new LinearScale();
var left = new YAxis(yScale, "left");
var data = makeRandomData(1000, 200);
var renderer = new CircleRenderer(data, xScale, yScale);
var bottomAxis = new XAxis(xScale, "bottom");
var xSpark = new LinearScale();
var ySpark = new LinearScale();
var sparkline = new LineRenderer(data, xSpark, ySpark);
sparkline.rowWeight(0.3);

var r1: Component[] = [left, renderer];
var r2: Component[] = [null, bottomAxis];
var r3: Component[] = [null, sparkline];

var chart = new Table([r1, r2, r3]);
chart.xMargin = 10;
chart.yMargin = 10;

var brushZoom = new BrushZoomInteraction(sparkline, xScale, yScale);
var toggleClass = function() {return !d3.select(this).classed("selected-point")};
var cb = (s) => s.classed("selected-point", toggleClass);
var areaInteraction = new AreaInteraction(renderer, null, cb);

var svg = d3.select("#table");
chart.anchor(svg);
chart.computeLayout();
chart.render();


}
18 changes: 18 additions & 0 deletions examples/tsc-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<link href="../style.css" type="text/css" rel="stylesheet" />
</head>
<body>
<h1> Basic TSC </h1>
<svg id="table" width="800px" height="600px"></svg><br><hr><br>


<script type="text/javascript"> window.demoName = "tsc-demo"; //HACK HACK </script>
<script src="../Lib/chai/chai.js"></script>
<script src="../Lib/lodash.js"></script>
<script src="../Lib/d3.ascii.js"></script>
<script src="../plottable.js"></script>
<script src="examples.js"></script>

</body>
32 changes: 32 additions & 0 deletions examples/tscDemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
///<reference path="../lib/d3.d.ts" />

///<reference path="../src/table.ts" />
///<reference path="../src/renderer.ts" />
///<reference path="../src/interaction.ts" />
///<reference path="../src/labelComponent.ts" />
///<reference path="../src/axis.ts" />
///<reference path="../src/scale.ts" />
///<reference path="exampleUtil.ts" />

if ((<any> window).demoName === "tsc-demo") {

var yScale = new LinearScale();
var xScale = new LinearScale();
var left = new YAxis(yScale, "left");
var data = makeRandomData(1000, 200);
var renderer = new LineRenderer(data, xScale, yScale);
var bottomAxis = new XAxis(xScale, "bottom");

var chart = new Table([[left, renderer]
,[null, bottomAxis]]);

var outerTable = new Table([ [new TitleLabel("A Chart")],
[chart] ])
outerTable.xMargin = 10;
outerTable.yMargin = 10;

var svg = d3.select("#table");
outerTable.anchor(svg);
outerTable.computeLayout();
outerTable.render();
}
19 changes: 8 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
@@ -4,18 +4,15 @@
<script src="Lib/d3.ascii.js"></script>
<script src="Lib/lodash.js"></script>
<script src="Lib/chai/chai.js"></script>
<script src="plottable.js"></script>
<link href="style.css" type="text/css" rel="stylesheet" />
</head>
<body>
<h1><a href="tests.html">HERE THERE BE TESTS</a></h1>
<h1> Basic TSC </h1>
<svg id="svg1"></svg><br><hr><br>
<h1> Chartbag of timeseriescharts </h1>
<svg id="svg2"></svg><br><hr><br>
<h1> TSC with 2 axes </h1>
<svg id="svg3"></svg><br><hr><br>
<h1> TSC with subplots, varying # of axes, and sparkline </h1>
<svg id="svg4"></svg><br><hr><br>
<svg id="svg5"></svg><br><hr><br>
<script src="plottable.js"></script>
<h1><a href="tests.html">Tests</a></h1>
<h1><a href="examples/demo1.html">The old demo</a></h1>
<h1><a href="examples/sparkline-demo.html">Sparkline demo</a></h1>
<h1><a href="examples/demo-day.html">Demo Day</a></h1>
<h1><a href="examples/demo-day-crazy.html">Demo Day With Crazy CSS</a></h1>
<h1><a href="examples/tsc-demo.html">TSC Demo</a></h1> <a href="examples/tscDemo.ts">(the code)</a>

</body>
11 changes: 4 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
{
"name": "linkedaxisprototype",
"version": "0.0.0",
"description": "Prototype linked axis timeseriescharts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"name": "plottable",
"version": "0.1.0",
"description": "A library for easily creating charts within a grid layout.",
"repository": {
"type": "git",
"url": "https://github.com/danmane/LinkedChartPrototype.git"
"url": "https://github.com/palantir/plottable.git"
},
"author": "daniel mane",
"devDependencies": {
29 changes: 21 additions & 8 deletions src/axis.ts
Original file line number Diff line number Diff line change
@@ -32,11 +32,13 @@ class Axis extends Component {
this.isXAligned = this.orientation === "bottom" || this.orientation === "top";
this.d3axis = d3.svg.axis().scale(this.scale.scale).orient(this.orientation);
if (this.formatter == null) {
this.formatter = d3.format("s3");
this.formatter = d3.format(".3s");
}
this.d3axis.tickFormat(this.formatter);

this.cachedScale = 1;
this.cachedTranslate = 0;
this.scale.registerListener(() => this.rescale());
}

private transformString(translate: number, scale: number) {
@@ -48,7 +50,7 @@ class Axis extends Component {
public rowWeight(newVal: number): Component;
public rowWeight(newVal?: number): any {
if (newVal != null) {
throw new Error("Axis row weight is not settable.");
throw new Error("Row weight cannot be set on Axis.");
return this;
} else {
return 0;
@@ -59,7 +61,7 @@ class Axis extends Component {
public colWeight(newVal: number): Component;
public colWeight(newVal?: number): any {
if (newVal != null) {
throw new Error("Axis col weight is not settable.");
throw new Error("Col weight cannot be set on Axis.");
return this;
} else {
return 0;
@@ -86,11 +88,19 @@ class Axis extends Component {
} else {
newDomain = standardOrder ? [new Date(min - extent), new Date(max + extent)] : [new Date(max + extent), new Date(min - extent)];
}
// var copyScale = this.scale.copy().domain(newDomain)
// var ticks = (<any> copyScale).ticks(30);
// this.d3axis.tickValues(ticks);
// a = [100,0]; extent = -100; 100 - (-100) = 200, 0 - (-100) = 100
// a = [0,100]; extent = 100; 0 - 100 = -100, 100 - 100

// Make tiny-zero representations not look like crap, by rounding them to 0
if ((<QuantitiveScale> this.scale).ticks != null) {
var scale = <QuantitiveScale> this.scale;
var nTicks = 10;
var ticks = scale.ticks(nTicks);
var domain = scale.domain();
var interval = domain[1] - domain[0];
var cleanTick = (n) => Math.abs(n / interval / nTicks) < 0.0001 ? 0 : n;
ticks = ticks.map(cleanTick);
this.d3axis.tickValues(ticks);
}

this.axisElement.call(this.d3axis);
var bbox = (<any> this.axisElement.node()).getBBox();
if (bbox.height > this.availableHeight || bbox.width > this.availableWidth) {
@@ -101,13 +111,16 @@ class Axis extends Component {
}

public rescale() {
return (this.element != null) ? this.render() : null;
// short circuit, we don't care about perf.
var tickTransform = this.isXAligned ? Axis.axisXTransform : Axis.axisYTransform;
var tickSelection = this.element.selectAll(".tick");
(<any> tickSelection).call(tickTransform, this.scale.scale);
this.axisElement.attr("transform","");
}

public zoom(translatePair: number[], scale: number) {
return this.render(); //short-circuit, we dont need the performant cleverness for present demo
var translate = this.isXAligned ? translatePair[0] : translatePair[1];
if (scale != null && scale != this.cachedScale) {
this.cachedTranslate = translate;
96 changes: 93 additions & 3 deletions src/component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
///<reference path="../lib/d3.d.ts" />
///<reference path="interaction.ts" />

class Component {
private static clipPathId = 0; // Used for unique namespacing for the clipPaths
public element: D3.Selection;
public hitBox: D3.Selection;
public boundingBox: D3.Selection;
private clipPathRect: D3.Selection;
private registeredInteractions: Interaction[] = [];

private rowWeightVal = 0;
private colWeightVal = 0;
@@ -13,10 +19,54 @@ class Component {
private xOffset : number;
private yOffset : number;

private cssClasses: string[] = [];

public xAlignment = "LEFT"; // LEFT, CENTER, RIGHT
public yAlignment = "TOP"; // TOP, CENTER, BOTTOM

public classed(cssClass: string): boolean;
public classed(cssClass: string, addClass: boolean): Component;
public classed(cssClass: string, addClass?:boolean): any {
if (addClass == null) {
if (this.element == null) {
return (this.cssClasses.indexOf(cssClass) != -1);
} else {
return this.element.classed(cssClass);
}
} else {
if (this.element == null) {
var classIndex = this.cssClasses.indexOf(cssClass);
if (addClass && classIndex == -1) {
this.cssClasses.push(cssClass);
} else if (!addClass && classIndex != -1) {
this.cssClasses.splice(classIndex, 1);
}
} else {
this.element.classed(cssClass, addClass);
}
return this;
}
}

public anchor(element: D3.Selection) {
this.element = element;
this.generateClipPath();
this.cssClasses.forEach((cssClass: string) => {
this.element.classed(cssClass, true);
});
this.cssClasses = null;
this.hitBox = element.append("rect").classed("hit-box", true);
this.boundingBox = element.append("rect").classed("bounding-box", true);
this.registeredInteractions.forEach((r) => r.anchor(this.hitBox));
}

public generateClipPath() {
// The clip path will prevent content from overflowing its component space.
var clipPathId = Component.clipPathId++;
this.element.attr("clip-path", "url(#clipPath" + clipPathId + ")");
this.clipPathRect = this.element.append("clipPath")
.attr("id", "clipPath" + clipPathId)
.append("rect");
}

public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) {
@@ -33,13 +83,53 @@ class Component {
throw new Error("You need to pass non-null arguments when calling computeLayout on a non-root node");
}
}
if (this.rowWeight() === 0 && this.rowMinimum() !== 0) {
switch (this.yAlignment) {
case "TOP":
break;
case "CENTER":
yOffset += (availableHeight - this.rowMinimum()) / 2;
break;
case "BOTTOM":
yOffset += availableHeight - this.rowMinimum();
break;
default:
throw new Error("unsupported alignment");
}
availableHeight = this.rowMinimum();
}
if (this.colWeight() === 0 && this.colMinimum() !== 0) {
switch (this.xAlignment) {
case "LEFT":
break;
case "CENTER":
xOffset += (availableWidth - this.colMinimum()) / 2;
break;
case "RIGHT":
xOffset += availableWidth - this.colMinimum();
break;
default:
throw new Error("unsupported alignment");
}
availableWidth = this.colMinimum();
}
this.xOffset = xOffset;
this.yOffset = yOffset;
this.availableWidth = availableWidth;
this.availableHeight = availableHeight;
this.element.attr("transform", "translate(" + this.xOffset + "," + this.yOffset + ")");
this.hitBox.attr("width", this.availableWidth).attr("height", this.availableHeight);
this.boundingBox.attr("width", this.availableWidth).attr("height", this.availableHeight);
var boxes = [this.clipPathRect, this.hitBox, this.boundingBox];
Utils.setWidthHeight(boxes, this.availableWidth, this.availableHeight);
}

public registerInteraction(interaction: Interaction) {
// Interactions can be registered before or after anchoring. If registered before, they are
// pushed to this.registeredInteractions and registered during anchoring. If after, they are
// registered immediately
this.registeredInteractions.push(interaction);
if (this.element != null) {
interaction.anchor(this.hitBox);
}
}

public render() {
@@ -70,7 +160,7 @@ class Component {
chai.assert.operator(this.colWeightVal, '>=', 0, "colWeight is a reasonable number");
return this;
} else {
return this.colWeightVal
return this.colWeightVal;
}
}

182 changes: 177 additions & 5 deletions src/interaction.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,194 @@
///<reference path="../lib/lodash.d.ts" />

class DragZoomInteraction {
class Interaction {
/* A general base class for interactions.
It maintains a 'hitBox' which is where all event listeners are attached. Due to cross-
browser weirdness, the hitbox needs to be an opaque but invisible rectangle.
TODO: We should give the interaction "foreground" and "background" elements where it can
draw things, e.g. crosshairs.
*/
public hitBox: D3.Selection;

constructor(public componentToListenTo: Component) {
}

public anchor(hitBox: D3.Selection) {
this.hitBox = hitBox;
}

public registerWithComponent() {
this.componentToListenTo.registerInteraction(this);
// It would be nice to have a call to this in the Interaction constructor, but
// can't do this right now because that depends on listenToHitBox being callable, which depends on the subclass
// constructor finishing first.
}
}

interface ZoomInfo {
translate: number[];
scale: number[];
}

class PanZoomInteraction extends Interaction {
private zoom;
constructor(public elementToListenTo: D3.Selection, public renderers: Component[], public xScale: Scale, public yScale: Scale) {
constructor(componentToListenTo: Component, public renderers: Component[], public xScale: QuantitiveScale, public yScale: QuantitiveScale) {
super(componentToListenTo);
this.zoom = d3.behavior.zoom();
this.zoom(elementToListenTo);
this.zoom.x(this.xScale.scale);
this.zoom.y(this.yScale.scale);
var throttledZoom = _.throttle(() => this.rerenderZoomed(), 30);
var throttledZoom = _.throttle(() => this.rerenderZoomed(), 16);
this.zoom.on("zoom", throttledZoom);

this.registerWithComponent();
}

public anchor(hitBox: D3.Selection) {
super.anchor(hitBox);
this.zoom(hitBox);
}

private rerenderZoomed() {
var translate = this.zoom.translate();
console.log(translate);
var scale = this.zoom.scale();
this.renderers.forEach((r) => {
r.zoom(translate, scale);
})
}
}

class AreaInteraction extends Interaction {
/*
This class is responsible for any kind of interaction in which you brush over an area
of a renderer and plan to execute some logic based on the selected area.
Right now it only works for XYRenderers, but we can make the interface more general in
the future.
You pass it a rendererComponent (:XYRenderer) and it sets up events so that you can draw
a rectangle over it. Then, you pass it callbacks that the AreaInteraction will execute on
the selected region. The first callback (areaCallback) will be passed a FullSelectionArea
object which contains info on both the pixel and data range of the selected region.
The selectionCallback will be passed a D3.Selection object that contains the elements bound
to the data in the selection region. You can use this, for example, to change their class
and display properties.
*/
private static CLASS_DRAG_BOX = "drag-box";
private dragInitialized = false;
private dragBehavior;
private origin = [0,0];
private location = [0,0];
private constrainX: (n: number) => number;
private constrainY: (n: number) => number;
private dragBox: D3.Selection;

constructor(
private rendererComponent: XYRenderer,
public areaCallback?: (a: FullSelectionArea) => any,
public selectionCallback?: (a: D3.Selection) => any,
public indicesCallback?: (a: number[]) => any
) {
super(rendererComponent);
this.dragBehavior = d3.behavior.drag();
this.dragBehavior.on("dragstart", () => this.dragstart());
this.dragBehavior.on("drag", () => this.drag());
this.dragBehavior.on("dragend", () => this.dragend());
this.registerWithComponent();
}

private dragstart(){
this.dragBox.attr("height", 0).attr("width", 0);
var availableWidth = parseFloat(this.hitBox.attr("width"));
var availableHeight = parseFloat(this.hitBox.attr("height"));
// the constraint functions ensure that the selection rectangle will not exceed the hit box
var constraintFunction = (min, max) => (x) => Math.min(Math.max(x, min), max);
this.constrainX = constraintFunction(0, availableWidth);
this.constrainY = constraintFunction(0, availableHeight);
}

private drag(){
if (!this.dragInitialized) {
this.origin = [d3.event.x, d3.event.y];
this.dragInitialized = true;
}

this.location = [this.constrainX(d3.event.x), this.constrainY(d3.event.y)];
var width = Math.abs(this.origin[0] - this.location[0]);
var height = Math.abs(this.origin[1] - this.location[1]);
var x = Math.min(this.origin[0], this.location[0]);
var y = Math.min(this.origin[1], this.location[1]);
this.dragBox.attr("x", x).attr("y", y).attr("height", height).attr("width", width);
}

private dragend(){
if (!this.dragInitialized) {
return;
// It records a tap as a dragstart+dragend, but this can have unintended consequences.
// only trigger logic if we actually did some dragging.
}
this.dragInitialized = false;
var xMin = Math.min(this.origin[0], this.location[0]);
var xMax = Math.max(this.origin[0], this.location[0]);
var yMin = Math.min(this.origin[1], this.location[1]);
var yMax = Math.max(this.origin[1], this.location[1]);
var pixelArea = {xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax};
var dataArea = this.rendererComponent.invertXYSelectionArea(pixelArea);
var fullArea = {pixel: pixelArea, data: dataArea};
if (this.areaCallback != null) {
this.areaCallback(fullArea);
}
if (this.selectionCallback != null) {
var selection = this.rendererComponent.getSelectionFromArea(fullArea);
this.selectionCallback(selection);
}
if (this.indicesCallback != null) {
var indices = this.rendererComponent.getDataIndicesFromArea(fullArea);
this.indicesCallback(indices);
}
}

public clearBox() {
this.dragBox.attr("height", 0).attr("width", 0);
}

public anchor(hitBox: D3.Selection) {
super.anchor(hitBox);
var cname = AreaInteraction.CLASS_DRAG_BOX;
var element = this.componentToListenTo.element;
this.dragBox = element.append("rect").classed(cname, true).attr("x", 0).attr("y", 0);
hitBox.call(this.dragBehavior);
}
}

class BrushZoomInteraction extends AreaInteraction {
/*
This is an extension of the AreaInteraction which is used for zooming into a selected region.
It takes the XYRenderer to initialize the AreaInteraction on, and the xScale and yScale to be
scaled according to the domain of the data selected. Note that the xScale and yScale given to
the BrushZoomInteraction can be distinct from those that the renderer depends on, e.g. if you
make a sparkline, you do not want to update the sparkline's scales, but rather the scales of a
linked chart.
*/
constructor(eventComponent: XYRenderer, public xScale: QuantitiveScale, public yScale: QuantitiveScale, public indicesCallback?: (a: number[]) => any) {
super(eventComponent);
this.areaCallback = this.zoom;
this.indicesCallback = indicesCallback;
}

public zoom(area: FullSelectionArea) {
var originalXDomain = this.xScale.domain();
var originalYDomain = this.yScale.domain();
var xDomain = [area.data.xMin, area.data.xMax];
var yDomain = [area.data.yMin, area.data.yMax];

var xOrigDirection = originalXDomain[0] > originalXDomain[1];
var yOrigDirection = originalYDomain[0] > originalYDomain[1];
var xDirection = xDomain[0] > xDomain[1];
var yDirection = yDomain[0] > yDomain[1]
// make sure we don't change inversion of the scale by zooming

if (xDirection != xOrigDirection) {xDomain.reverse();};
if (yDirection != yOrigDirection) {yDomain.reverse();};


this.xScale.domain(xDomain);
this.yScale.domain(yDomain);
}
}
21 changes: 21 additions & 0 deletions src/interfaces.d.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,27 @@ interface IDataset {
seriesName: string;
}

interface SelectionArea {
xMin: number;
xMax: number;
yMin: number;
yMax: number;
}

interface FullSelectionArea {
pixel: SelectionArea;
data: SelectionArea;
}


interface IBroadcasterCallback {
(broadcaster: IBroadcaster, ...args: any[]): any;
}

interface IBroadcaster {
registerListener: (cb: IBroadcasterCallback) => IBroadcaster;
}

// interface IRenderer<T extends IDatum> extends IRendererable {

// }
111 changes: 111 additions & 0 deletions src/labelComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
///<reference path="../lib/d3.d.ts" />

class LabelComponent extends Component {
public CLASS_TEXT_LABEL = "text-label";

public xAlignment = "CENTER";
public yAlignment = "CENTER";

private textElement: D3.Selection;
private text:string;
private textHeight = 0;
private textWidth = 0;
private isVertical = false;
private rotationAngle = 0;
private orientation = "horizontal";

constructor(text: string, orientation?: string) {
super();
this.classed(this.CLASS_TEXT_LABEL, true);
this.text = text;
if (orientation === "horizontal" || orientation === "vertical-left" || orientation === "vertical-right") {
this.orientation = orientation;
} else if (orientation != null) {
throw new Error(orientation + " is not a valid orientation for LabelComponent");
}
}

public rowWeight(): number;
public rowWeight(newVal: number): Component;
public rowWeight(newVal?: number): any {
if (newVal != null) {
throw new Error("Row weight cannot be set on Label.");
return this;
} else {
return 0;
}
}

public colWeight(): number;
public colWeight(newVal: number): Component;
public colWeight(newVal?: number): any {
if (newVal != null) {
throw new Error("Col weight cannot be set on Label.");
return this;
} else {
return 0;
}
}

public rowMinimum(): number;
public rowMinimum(newVal: number): Component;
public rowMinimum(newVal?: number): any {
if (newVal != null) {
throw new Error("Row minimum cannot be directly set on Label.");
return this;
} else {
return this.textHeight;
}
}

public colMinimum(): number;
public colMinimum(newVal: number): Component;
public colMinimum(newVal?: number): any {
if (newVal != null) {
throw new Error("Col minimum cannot be directly set on Label.");
return this;
} else {
return this.textWidth;
}
}

public anchor(element: D3.Selection) {
super.anchor(element);
this.textElement = this.element.append("text")
.attr("alignment-baseline", "middle")
.text(this.text);

var clientHeight = this.textElement.node().clientHeight;
var clientWidth = this.textElement.node().clientWidth;

if (this.orientation === "horizontal") {
this.textElement.attr("transform", "translate(0 " + clientHeight/2 + ")");
this.textHeight = clientHeight;
this.textWidth = clientWidth;
} else {
this.textWidth = clientHeight;
this.textHeight = clientWidth;
if (this.orientation === "vertical-right") {
this.textElement.attr("transform", "rotate(90) translate(0 " + (-clientHeight/2) + ")");
} else if (this.orientation === "vertical-left") {
this.textElement.attr("transform", "rotate(-90) translate(" + (-clientWidth) + " " + clientHeight/2 + ")");
}
}
}
}

class TitleLabel extends LabelComponent {
public CLASS_TITLE_LABEL = "title-label";
constructor(text: string, orientation?: string) {
super(text, orientation);
this.classed(this.CLASS_TITLE_LABEL, true);
}
}

class AxisLabel extends LabelComponent {
public CLASS_AXIS_LABEL = "axis-label";
constructor(text: string, orientation?: string) {
super(text, orientation);
this.classed(this.CLASS_AXIS_LABEL, true);
}
}
151 changes: 134 additions & 17 deletions src/renderer.ts
Original file line number Diff line number Diff line change
@@ -3,16 +3,27 @@
///<reference path="scale.ts" />

class Renderer extends Component {
public CLASS_RENDERER_CONTAINER = "renderer-container";

public dataset: IDataset;
public renderArea: D3.Selection;
public element: D3.Selection;
public scales: Scale[];

constructor(
public dataset: IDataset
dataset: IDataset
) {
super();
super.rowWeight(1);
super.colWeight(1);

this.dataset = dataset;
this.classed(this.CLASS_RENDERER_CONTAINER, true);
}

public data(dataset: IDataset): Renderer {
this.dataset = dataset;
return this;
}

public zoom(translate, scale) {
@@ -21,7 +32,6 @@ class Renderer extends Component {

public anchor(element: D3.Selection) {
super.anchor(element);
this.element.classed("renderer-container", true);
this.boundingBox.classed("renderer-bounding-box", true);
this.renderArea = element.append("g").classed("render-area", true).classed(this.dataset.seriesName, true);
}
@@ -32,15 +42,16 @@ interface IAccessor {
};

class XYRenderer extends Renderer {
public dataSelection: D3.Selection;
private static defaultXAccessor = (d: any) => d.x;
private static defaultYAccessor = (d: any) => d.y;
public xScale: Scale;
public yScale: Scale;
public xScale: QuantitiveScale;
public yScale: QuantitiveScale;
private xAccessor: IAccessor;
private yAccessor: IAccessor;
public xScaledAccessor: (datum: any) => number;
public yScaledAccessor: (datum: any) => number;
constructor(dataset: IDataset, xScale: Scale, yScale: Scale, xAccessor?: IAccessor, yAccessor?: IAccessor) {
public xScaledAccessor: IAccessor;
public yScaledAccessor: IAccessor;
constructor(dataset: IDataset, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: IAccessor, yAccessor?: IAccessor) {
super(dataset);
this.xAccessor = (xAccessor != null) ? xAccessor : XYRenderer.defaultXAccessor;
this.yAccessor = (yAccessor != null) ? yAccessor : XYRenderer.defaultYAccessor;
@@ -53,20 +64,75 @@ class XYRenderer extends Renderer {
this.xScale.widenDomain(xDomain);
var yDomain = d3.extent(data, this.yAccessor);
this.yScale.widenDomain(yDomain);

this.xScale.registerListener(() => this.rescale());
this.yScale.registerListener(() => this.rescale());
}

public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight? :number) {
super.computeLayout(xOffset, yOffset, availableWidth, availableHeight);
this.xScale.range([0, this.availableWidth]);
this.yScale.range([this.availableHeight, 0]);
}

public invertXYSelectionArea(area: SelectionArea) {
var xMin = this.xScale.invert(area.xMin);
var xMax = this.xScale.invert(area.xMax);
var yMin = this.yScale.invert(area.yMin);
var yMax = this.yScale.invert(area.yMax);
return {xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax}
}

public getSelectionFromArea(area: FullSelectionArea) {

var dataArea = area.data;
var inRange = (x: number, a: number, b: number) => {
return (Math.min(a,b) <= x && x <= Math.max(a,b));
}
var filterFunction = (d: any) => {
var x = this.xAccessor(d);
var y = this.yAccessor(d);
// use inRange rather than direct comparison to avoid thinking about scale inversion
return inRange(x, dataArea.xMin, dataArea.xMax) && inRange(y, dataArea.yMin, dataArea.yMax);;
}
var selection = this.dataSelection.filter(filterFunction);
return selection;
}

public getDataIndicesFromArea(area: FullSelectionArea) {
var dataArea = area.data;
var inRange = (x: number, a: number, b: number) => {
return (Math.min(a,b) <= x && x <= Math.max(a,b));
}
var filterFunction = (d: any) => {
var x = this.xAccessor(d);
var y = this.yAccessor(d);
// use inRange rather than direct comparison to avoid thinking about scale inversion
return inRange(x, dataArea.xMin, dataArea.xMax) && inRange(y, dataArea.yMin, dataArea.yMax);;
}
var results = [];
this.dataset.data.forEach((d, i) => {
if (filterFunction(d)) {
results.push(i);
}
});
return results;
}

public rescale() {
if (this.element != null) {
this.renderArea.remove();
this.renderArea = this.element.append("g").classed("render-area", true).classed(this.dataset.seriesName, true);
this.render();
}
}
}


class LineRenderer extends XYRenderer {
private line: D3.Svg.Line;

constructor(dataset: IDataset, xScale: Scale, yScale: Scale, xAccessor?: IAccessor, yAccessor?: IAccessor) {
constructor(dataset: IDataset, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: IAccessor, yAccessor?: IAccessor) {
super(dataset, xScale, yScale, xAccessor, yAccessor);
}

@@ -78,34 +144,85 @@ class LineRenderer extends XYRenderer {
public render() {
super.render();
this.line = d3.svg.line().interpolate("basis").x(this.xScaledAccessor).y(this.yScaledAccessor);
this.renderArea.classed("line", true)
this.dataSelection = this.renderArea.classed("line", true)
.classed(this.dataset.seriesName, true)
.datum(this.dataset.data);
this.renderArea.attr("d", this.line);
}
}

class CircleRenderer extends XYRenderer {
private circles: D3.Selection;

constructor(dataset: IDataset, xScale: Scale, yScale: Scale, xAccessor?: IAccessor, yAccessor?: IAccessor) {
public size: number;
constructor(dataset: IDataset, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: IAccessor, yAccessor?: IAccessor, size=3) {
super(dataset, xScale, yScale, xAccessor, yAccessor);
this.size = size;
}

public render() {
super.render();

this.circles = this.renderArea.selectAll("circle");
this.circles.data(this.dataset.data).enter().append("circle")
this.dataSelection = this.renderArea.selectAll("circle");
this.dataSelection = this.dataSelection.data(this.dataset.data).enter()
.append("circle")
.attr("cx", this.xScaledAccessor)
.attr("cy", this.yScaledAccessor)
.attr("r", 3);
.attr("r", this.size)
.classed("selected-point", (d) => d.selected);
}
}

class BarRenderer extends XYRenderer {
private BAR_START_PADDING_PX = 1;
private BAR_END_PADDING_PX = 1;

private x2Accessor: IAccessor;
public x2ScaledAccessor: IAccessor;

constructor(dataset: IDataset,
xScale: QuantitiveScale,
yScale: QuantitiveScale,
xAccessor?: IAccessor,
x2Accessor?: IAccessor,
yAccessor?: IAccessor) {
super(dataset, xScale, yScale, xAccessor, yAccessor);

var inRange = (x: number, a: number, b: number) => {
return (Math.min(a,b) <= x && x <= Math.max(a,b));
}

var yDomain = this.yScale.domain();
if (!inRange(0, yDomain[0], yDomain[1])) {
var newMin = 0;
var newMax = 1.1 * yDomain[1];
this.yScale.widenDomain([newMin, newMax]); // TODO: make this handle reversed scales
}

this.x2Accessor = (x2Accessor != null) ? x2Accessor : (d: any) => d.x2;
this.x2ScaledAccessor = (datum: any) => xScale.scale(this.x2Accessor(datum));

var x2Extent = d3.extent(dataset.data, this.x2Accessor);
this.xScale.widenDomain(x2Extent);
}

public render() {
super.render();
var yRange = this.yScale.range();
var maxScaledY = Math.max(yRange[0], yRange[1]);

var dataSelection = this.renderArea.selectAll("rect").data(this.dataset.data);
dataSelection.enter().append("rect");
dataSelection.transition().attr("x", (d: any) => this.xScaledAccessor(d) + this.BAR_START_PADDING_PX)
.attr("y", this.yScaledAccessor)
.attr("width", (d: any) => (this.x2ScaledAccessor(d) - this.xScaledAccessor(d)
- this.BAR_START_PADDING_PX - this.BAR_END_PADDING_PX))
.attr("height", (d: any) => {
return (maxScaledY - this.yScaledAccessor(d));
});
dataSelection.exit().remove();
}
}

// class ResizingCircleRenderer extends CircleRenderer {
// public transform(translate: number[], scale: number) {
// console.log("xform");
// this.renderArea.selectAll("circle").attr("r", 0.5/scale);
// }
// }
62 changes: 57 additions & 5 deletions src/scale.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
///<reference path="../lib/d3.d.ts" />

class Scale {
class Scale implements IBroadcaster {
public scale: D3.Scale.Scale;
private broadcasterCallbacks: IBroadcasterCallback[] = [];

constructor(scale: D3.Scale.Scale) {
this.scale = scale;
@@ -14,8 +15,12 @@ class Scale {
public domain(): any[];
public domain(values: any[]): Scale;
public domain(values?: any[]): any {
if (values != null) {
return this.scale.domain(values);
if (values != null && !(_.isEqual(values, this.scale.domain()))) {
// It is important that the scale does not update if the new domain is the same as
// the current domain, to prevent circular propogation of events
this.scale.domain(values);
this.broadcasterCallbacks.forEach((b) => b(this));
return this;
} else {
return this.scale.domain();
}
@@ -25,7 +30,8 @@ class Scale {
public range(values: any[]): Scale;
public range(values?: any[]): any {
if (values != null) {
return this.scale.range(values);
this.scale.range(values);
return this;
} else {
return this.scale.range();
}
@@ -35,17 +41,63 @@ class Scale {
return new Scale(this.scale.copy());
}


public widenDomain(newDomain: number[]) {
var currentDomain = this.domain();
var wideDomain = [Math.min(newDomain[0], currentDomain[0]), Math.max(newDomain[1], currentDomain[1])];
this.domain(wideDomain);
return this;
}

public registerListener(callback: IBroadcasterCallback) {
this.broadcasterCallbacks.push(callback);
return this;
}
}

class QuantitiveScale extends Scale {
public scale: D3.Scale.QuantitiveScale;
constructor(scale: D3.Scale.QuantitiveScale) {
super(scale);
}

public invert(value: number) {
return this.scale.invert(value);
}

public ticks(count: number) {
return this.scale.ticks(count);
}
}

class LinearScale extends Scale {
class LinearScale extends QuantitiveScale {
constructor() {
super(d3.scale.linear());
this.domain([Infinity, -Infinity]);
}
}

class ScaleDomainCoordinator {
/* This class is responsible for maintaining coordination between linked scales.
It registers event listeners for when one of its scales changes its domain. When the scale
does change its domain, it re-propogates the change to every linked scale.
*/
private currentDomain: any[] = [];
constructor(private scales: Scale[]) {
this.scales.forEach((s) => s.registerListener((sx: Scale) => this.rescale(sx)));
}

public rescale(scale: Scale) {
var newDomain = scale.domain();
if (_.isEqual(newDomain, this.currentDomain)) {
// Avoid forming a really funky call stack with depth proportional to number of scales
return;
}
this.currentDomain = newDomain;
// This will repropogate the change to every scale, including the scale that
// originated it. This is fine because the scale will check if the new domain is
// different from its current one and will disregard the change if they are equal.
// It would be easy to stop repropogating to the original scale if it mattered.
this.scales.forEach((s) => s.domain(newDomain));
}
}
12 changes: 6 additions & 6 deletions src/table.ts
Original file line number Diff line number Diff line change
@@ -7,10 +7,10 @@


class Table extends Component {
public rowPadding = 5;
public colPadding = 5;
public xMargin = 5;
public yMargin = 5;
public rowPadding = 0;
public colPadding = 0;
public xMargin = 0;
public yMargin = 0;

private rows: Component[][];
private cols: Component[][];
@@ -106,10 +106,10 @@ class Table extends Component {
component.computeLayout(childXOffset, childYOffset, colWidths[colIndex], rowHeights[rowIndex]);
childXOffset += colWidths[colIndex] + this.colPadding;
});
chai.assert.operator(childXOffset - this.colPadding - this.xMargin, "<=", this.availableWidth, "final xOffset was <= availableWidth");
chai.assert.operator(childXOffset - this.colPadding - this.xMargin, "<=", this.availableWidth + 0.1, "final xOffset was <= availableWidth");
childYOffset += rowHeights[rowIndex] + this.rowPadding;
});
chai.assert.operator(childYOffset - this.rowPadding - this.yMargin, "<=", this.availableHeight, "final yOffset was <= availableHeight");
chai.assert.operator(childYOffset - this.rowPadding - this.yMargin, "<=", this.availableHeight + 0.1, "final yOffset was <= availableHeight");
}

private static rowProportionalSpace(rows: Component[][], freeHeight: number) {
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -21,4 +21,9 @@ module Utils {
export function getBBox(element: D3.Selection): SVGRect {
return (<any> element.node()).getBBox();
}
export function setWidthHeight(elements: D3.Selection[], width: number, height: number) {
elements.forEach((e) => {
e.attr("width", width).attr("height", height);
})
}
}
32 changes: 32 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
@@ -7,6 +7,23 @@ div {
display: inline;
}

.text-label text {
font-family: TrebuchetMS;
}

.axis-label text {
font-size: 18pt;
font-style: italic;
}

.title-label text {
font-size: 36pt;
}

.text-label-vertical {
writing-mode: tb;
}

.axis path,
.axis line {
fill: none;
@@ -30,6 +47,9 @@ div {
circle {
fill: steelblue;
}
rect {
fill: steelblue;
}

.table-rect {
fill: none;
@@ -59,6 +79,7 @@ circle {

.bounding-box {
fill: none;
stroke: black;
}

.renderer-bounding-box {
@@ -72,3 +93,14 @@ circle {
.table-bounding-box {
stroke: blue;
}

.drag-box {
fill: aliceblue;
opacity: 1;

}

.selected-point {
fill: orange;
stroke: none;
}
6 changes: 0 additions & 6 deletions test/axisTests.ts
Original file line number Diff line number Diff line change
@@ -6,9 +6,3 @@
///<reference path="../src/axis.ts" />

var assert = chai.assert;

describe("truthiness", () => {
it("true", () => {
assert.isTrue(true, "true is true!");
})
})
51 changes: 51 additions & 0 deletions test/componentTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
///<reference path="../lib/chai/chai.d.ts" />
///<reference path="../lib/chai/chai-assert.d.ts" />
///<reference path="../lib/mocha/mocha.d.ts" />
///<reference path="../lib/d3.d.ts" />

///<reference path="../src/axis.ts" />
///<reference path="../src/table.ts" />
///<reference path="../src/renderer.ts" />
///<reference path="../src/utils.ts" />
///<reference path="testUtils.ts" />

var assert = chai.assert;

function assertComponentXY(component: Component, x: number, y: number, message: string) {
// use <any> to examine the private variables
var xOffset = (<any> component).xOffset;
var yOffset = (<any> component).yOffset;
assert.equal(xOffset, x, message);
assert.equal(yOffset, y, message);
}

describe("Component behavior", () => {
it("fixed-width component will align to the right spot", () => {
var svg = generateSVG("300", "300");
var component = new Component();
component.rowMinimum(100).colMinimum(100);
component.anchor(svg);
component.computeLayout();
assertComponentXY(component, 0, 0, "top-left component aligns correctly");

component.xAlignment = "CENTER";
component.yAlignment = "CENTER";
component.computeLayout();
assertComponentXY(component, 100, 100, "center component aligns correctly");

component.xAlignment = "RIGHT";
component.yAlignment = "BOTTOM";
component.computeLayout();
assertComponentXY(component, 200, 200, "bottom-right component aligns correctly");
svg.remove();
})
it("component defaults are as expected", () => {
var c = new Component();
assert.equal(c.rowMinimum(), 0, "rowMinimum defaults to 0");
assert.equal(c.rowWeight() , 0, "rowWeight defaults to 0");
assert.equal(c.colMinimum(), 0, "colMinimum defaults to 0");
assert.equal(c.colWeight() , 0, "colWeight defaults to 0");
assert.equal(c.xAlignment, "LEFT", "xAlignment defaults to LEFT");
assert.equal(c.yAlignment, "TOP" , "yAlignment defaults to TOP");
})
})
8 changes: 2 additions & 6 deletions test/tableTests.ts
Original file line number Diff line number Diff line change
@@ -7,13 +7,10 @@
///<reference path="../src/table.ts" />
///<reference path="../src/renderer.ts" />
///<reference path="../src/utils.ts" />
///<reference path="testUtils.ts" />

var assert = chai.assert;

function generateSVG(width, height) {
return d3.select("body").append("svg:svg").attr("width", width).attr("height", height);
}

function generateBasicTable(nRows, nCols) {
// makes a table with exactly nRows * nCols children in a regular grid, with each
// child being a basic Renderer (todo: maybe change to basic component)
@@ -142,6 +139,5 @@ describe("Table layout", () => {
assertBBoxEquivalence(bboxes[5], [50, 340], "right axis bbox");
assertBBoxEquivalence(bboxes[4], [300, 340], "plot bbox");
svg.remove();
})

})
})
3 changes: 3 additions & 0 deletions test/testUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function generateSVG(width, height) {
return d3.select("body").append("svg:svg").attr("width", width).attr("height", height);
}

0 comments on commit 6851aba

Please sign in to comment.