diff --git a/.gitignore b/.gitignore
index 52a5975fc6..dd3b1ae5ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ plottable.js
plottable.js.map
tests.js
tests.js.map
+examples/*.js
+examples/*.js.map
diff --git a/Gruntfile.js b/Gruntfile.js
index 81556e809d..cfdff01a2e 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -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"
]
}
}
diff --git a/examples/demo-day-crazy.css b/examples/demo-day-crazy.css
new file mode 100644
index 0000000000..00304266cf
--- /dev/null
+++ b/examples/demo-day-crazy.css
@@ -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;
+}
diff --git a/examples/demo-day-crazy.html b/examples/demo-day-crazy.html
new file mode 100644
index 0000000000..ee114e7d2a
--- /dev/null
+++ b/examples/demo-day-crazy.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/demo-day.css b/examples/demo-day.css
new file mode 100644
index 0000000000..31c3578c3e
--- /dev/null
+++ b/examples/demo-day.css
@@ -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;
+}
diff --git a/examples/demo-day.html b/examples/demo-day.html
new file mode 100644
index 0000000000..486136301f
--- /dev/null
+++ b/examples/demo-day.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/demo.ts b/examples/demo.ts
similarity index 61%
rename from src/demo.ts
rename to examples/demo.ts
index ef3e0bd7dd..914a5b7a3f 100644
--- a/src/demo.ts
+++ b/examples/demo.ts
@@ -1,22 +1,17 @@
///
///
+///
+
+///
+///
+///
+///
+///
+///
-///
-///
-///
-
-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 (( 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
+
+
+
+
+
+
+ Bar Renderer
+
+
+
+ Basic TSC
+
+ Chartbag of timeseriescharts
+
+ TSC with 2 axes
+
+ TSC with subplots, varying # of axes, and sparkline
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/demoDay.ts b/examples/demoDay.ts
new file mode 100644
index 0000000000..4f9dfee525
--- /dev/null
+++ b/examples/demoDay.ts
@@ -0,0 +1,140 @@
+///
+
+///
+///
+///
+///
+///
+///
+///
+
+if (( 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();
+
+}
diff --git a/examples/exampleUtil.ts b/examples/exampleUtil.ts
new file mode 100644
index 0000000000..9a31034a68
--- /dev/null
+++ b/examples/exampleUtil.ts
@@ -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 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 {
+ 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"
+ };
+}
diff --git a/examples/sparkline-demo.html b/examples/sparkline-demo.html
new file mode 100644
index 0000000000..5070e53026
--- /dev/null
+++ b/examples/sparkline-demo.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Basic TSC
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/sparklineDemo.ts b/examples/sparklineDemo.ts
new file mode 100644
index 0000000000..7b6f8fcc03
--- /dev/null
+++ b/examples/sparklineDemo.ts
@@ -0,0 +1,43 @@
+///
+
+///
+///
+///
+///
+///
+///
+///
+
+if (( 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();
+
+
+}
diff --git a/examples/tsc-demo.html b/examples/tsc-demo.html
new file mode 100644
index 0000000000..fae6819b04
--- /dev/null
+++ b/examples/tsc-demo.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Basic TSC
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/tscDemo.ts b/examples/tscDemo.ts
new file mode 100644
index 0000000000..8775cfabb1
--- /dev/null
+++ b/examples/tscDemo.ts
@@ -0,0 +1,32 @@
+///
+
+///
+///
+///
+///
+///
+///
+///
+
+if (( 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();
+}
diff --git a/index.html b/index.html
index 96f162451a..d9c8deee16 100644
--- a/index.html
+++ b/index.html
@@ -4,18 +4,15 @@
+
-
- Basic TSC
-
- Chartbag of timeseriescharts
-
- TSC with 2 axes
-
- TSC with subplots, varying # of axes, and sparkline
-
-
-
+
+
+
+
+
+ (the code)
+
diff --git a/package.json b/package.json
index 6af3715ef5..5387c1a968 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/axis.ts b/src/axis.ts
index d6327baacd..37c5cec639 100644
--- a/src/axis.ts
+++ b/src/axis.ts
@@ -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 = ( 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 (( this.scale).ticks != null) {
+ var scale = 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 = ( this.axisElement.node()).getBBox();
if (bbox.height > this.availableHeight || bbox.width > this.availableWidth) {
@@ -101,6 +111,8 @@ 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");
( tickSelection).call(tickTransform, this.scale.scale);
@@ -108,6 +120,7 @@ class Axis extends Component {
}
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;
diff --git a/src/component.ts b/src/component.ts
index 93f5b6ca17..4e024923c6 100644
--- a/src/component.ts
+++ b/src/component.ts
@@ -1,7 +1,13 @@
+///
+///
+
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;
}
}
diff --git a/src/interaction.ts b/src/interaction.ts
index 62e8fcf0af..ffdde85ba8 100644
--- a/src/interaction.ts
+++ b/src/interaction.ts
@@ -1,22 +1,194 @@
///
-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);
+ }
+}
diff --git a/src/interfaces.d.ts b/src/interfaces.d.ts
index b124f0b5e9..1bb35e2345 100644
--- a/src/interfaces.d.ts
+++ b/src/interfaces.d.ts
@@ -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 extends IRendererable {
// }
diff --git a/src/labelComponent.ts b/src/labelComponent.ts
new file mode 100644
index 0000000000..50136bf9e3
--- /dev/null
+++ b/src/labelComponent.ts
@@ -0,0 +1,111 @@
+///
+
+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);
+ }
+}
diff --git a/src/renderer.ts b/src/renderer.ts
index 2f1db14f0a..7241865d1b 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -3,16 +3,27 @@
///
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,6 +64,9 @@ 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) {
@@ -60,13 +74,65 @@ class XYRenderer extends Renderer {
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,7 +144,7 @@ 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);
@@ -86,26 +152,77 @@ class LineRenderer extends XYRenderer {
}
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);
// }
// }
diff --git a/src/scale.ts b/src/scale.ts
index fcb393077b..15d45592e4 100644
--- a/src/scale.ts
+++ b/src/scale.ts
@@ -1,7 +1,8 @@
///
-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));
+ }
+}
diff --git a/src/table.ts b/src/table.ts
index c16a8b608d..86e76241d4 100644
--- a/src/table.ts
+++ b/src/table.ts
@@ -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) {
diff --git a/src/utils.ts b/src/utils.ts
index 561282d602..7dba96dcb6 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -21,4 +21,9 @@ module Utils {
export function getBBox(element: D3.Selection): SVGRect {
return ( element.node()).getBBox();
}
+ export function setWidthHeight(elements: D3.Selection[], width: number, height: number) {
+ elements.forEach((e) => {
+ e.attr("width", width).attr("height", height);
+ })
+ }
}
diff --git a/style.css b/style.css
index 85329cac36..772376b46b 100644
--- a/style.css
+++ b/style.css
@@ -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;
+}
diff --git a/test/axisTests.ts b/test/axisTests.ts
index d4701073a3..8d404b370a 100644
--- a/test/axisTests.ts
+++ b/test/axisTests.ts
@@ -6,9 +6,3 @@
///
var assert = chai.assert;
-
-describe("truthiness", () => {
- it("true", () => {
- assert.isTrue(true, "true is true!");
- })
-})
diff --git a/test/componentTests.ts b/test/componentTests.ts
new file mode 100644
index 0000000000..517f447cc3
--- /dev/null
+++ b/test/componentTests.ts
@@ -0,0 +1,51 @@
+///
+///
+///
+///
+
+///
+///
+///
+///
+///
+
+var assert = chai.assert;
+
+function assertComponentXY(component: Component, x: number, y: number, message: string) {
+ // use to examine the private variables
+ var xOffset = ( component).xOffset;
+ var yOffset = ( 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");
+ })
+})
diff --git a/test/tableTests.ts b/test/tableTests.ts
index c2fc094c4f..d5d94e4984 100644
--- a/test/tableTests.ts
+++ b/test/tableTests.ts
@@ -7,13 +7,10 @@
///
///
///
+///
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();
- })
-
+ })
})
diff --git a/test/testUtils.ts b/test/testUtils.ts
new file mode 100644
index 0000000000..2480827771
--- /dev/null
+++ b/test/testUtils.ts
@@ -0,0 +1,3 @@
+function generateSVG(width, height) {
+ return d3.select("body").append("svg:svg").attr("width", width).attr("height", height);
+}