From b9cfa458a6535e7ee0ff8b81398afa1e123cf458 Mon Sep 17 00:00:00 2001
From: Andrew Kim <andrewdkim@users.noreply.github.com>
Date: Sat, 9 Mar 2019 18:21:06 -0500
Subject: added audio and video nodes

---
 src/client/views/nodes/FieldView.tsx | 11 +++++++++++
 1 file changed, 11 insertions(+)

(limited to 'src/client/views/nodes/FieldView.tsx')

diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 9e63006d1..49f4cefce 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -8,10 +8,15 @@ import { NumberField } from "../../../fields/NumberField";
 import { RichTextField } from "../../../fields/RichTextField";
 import { ImageField } from "../../../fields/ImageField";
 import { WebField } from "../../../fields/WebField";
+import { VideoField } from "../../../fields/VideoField"
 import { Key } from "../../../fields/Key";
 import { FormattedTextBox } from "./FormattedTextBox";
 import { ImageBox } from "./ImageBox";
 import { WebBox } from "./WebBox";
+import { VideoBox } from "./VideoBox";
+import { AudioBox } from "./AudioBox";
+import { AudioField } from "../../../fields/AudioField";
+
 
 //
 // these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -55,6 +60,12 @@ export class FieldView extends React.Component<FieldViewProps> {
         }
         else if (field instanceof WebField) {
             return <WebBox {...this.props} />
+        } 
+        else if (field instanceof VideoField){ 
+            return <VideoBox {...this.props}/>
+        }
+        else if (field instanceof AudioField){ 
+            return <AudioBox {...this.props}/>
         }
         // bcz: this belongs here, but it doesn't render well so taking it out for now
         // else if (field instanceof HtmlField) {
-- 
cgit v1.2.3-70-g09d2


From 1ae0ba5addbcf3d527f1e75cc895aaf023063a7d Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Mon, 18 Mar 2019 14:05:37 -0400
Subject: Cleaned up field view

---
 src/client/views/nodes/FieldView.tsx | 19 ++++++++++++++-----
 src/fields/Document.ts               |  1 +
 2 files changed, 15 insertions(+), 5 deletions(-)

(limited to 'src/client/views/nodes/FieldView.tsx')

diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 49f4cefce..e84c5f933 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -16,6 +16,7 @@ import { WebBox } from "./WebBox";
 import { VideoBox } from "./VideoBox";
 import { AudioBox } from "./AudioBox";
 import { AudioField } from "../../../fields/AudioField";
+import { ListField } from "../../../fields/ListField";
 
 
 //
@@ -60,12 +61,20 @@ export class FieldView extends React.Component<FieldViewProps> {
         }
         else if (field instanceof WebField) {
             return <WebBox {...this.props} />
-        } 
-        else if (field instanceof VideoField){ 
-            return <VideoBox {...this.props}/>
         }
-        else if (field instanceof AudioField){ 
-            return <AudioBox {...this.props}/>
+        else if (field instanceof VideoField) {
+            return <VideoBox {...this.props} />
+        }
+        else if (field instanceof AudioField) {
+            return <AudioBox {...this.props} />
+        } else if (field instanceof Document) {
+            return <div>{field.Title}</div>
+        } else if (field instanceof ListField) {
+            return (<div>
+                {(field as ListField<Field>).Data.map(f => {
+                    return f instanceof Document ? f.Title : f.GetValue().toString();
+                }).join(", ")}
+            </div>)
         }
         // bcz: this belongs here, but it doesn't render well so taking it out for now
         // else if (field instanceof HtmlField) {
diff --git a/src/fields/Document.ts b/src/fields/Document.ts
index 5f0889786..be0137128 100644
--- a/src/fields/Document.ts
+++ b/src/fields/Document.ts
@@ -310,6 +310,7 @@ export class Document extends Field {
         throw new Error("Method not implemented.");
     }
     GetValue() {
+        return this.Title;
         var title = (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) + "(" + this.Id + ")";
         return title;
         //throw new Error("Method not implemented.");
-- 
cgit v1.2.3-70-g09d2


From ab55b4745e4c30547f8b3e23dcc9af455241e7ae Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Mon, 18 Mar 2019 18:40:12 -0400
Subject: Got rid of web box in FieldView

---
 src/client/views/nodes/FieldView.tsx | 3 ---
 1 file changed, 3 deletions(-)

(limited to 'src/client/views/nodes/FieldView.tsx')

diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index e84c5f933..b6d50bffb 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -59,9 +59,6 @@ export class FieldView extends React.Component<FieldViewProps> {
         else if (field instanceof ImageField) {
             return <ImageBox {...this.props} />
         }
-        else if (field instanceof WebField) {
-            return <WebBox {...this.props} />
-        }
         else if (field instanceof VideoField) {
             return <VideoBox {...this.props} />
         }
-- 
cgit v1.2.3-70-g09d2


From a16e6592caafb601b59c3d9f7609e8c1af231eba Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 20 Mar 2019 18:00:39 -0400
Subject: initial

---
 package.json                                       |   2 +
 src/client/documents/Documents.ts                  |  14 +-
 src/client/northstar/core/BaseObject.ts            |  12 ++
 .../northstar/core/attribute/AttributeModel.ts     | 111 ++++++++++
 .../core/attribute/AttributeTransformationModel.ts |  47 ++++
 .../core/attribute/CalculatedAttributeModel.ts     |  42 ++++
 .../northstar/core/brusher/BrushLinkModel.ts       |  40 ++++
 .../northstar/core/brusher/IBaseBrushable.ts       |  13 ++
 src/client/northstar/core/brusher/IBaseBrusher.ts  |  11 +
 src/client/northstar/core/filter/FilterModel.ts    |  81 +++++++
 src/client/northstar/core/filter/FilterOperand.ts  |   5 +
 src/client/northstar/core/filter/FilterType.ts     |   6 +
 .../northstar/core/filter/IBaseFilterConsumer.ts   |  10 +
 .../northstar/core/filter/IBaseFilterProvider.ts   |   8 +
 .../northstar/core/filter/ValueComparision.ts      |  74 +++++++
 src/client/northstar/model/ModelExtensions.ts      |  48 +++++
 src/client/northstar/model/ModelHelpers.ts         | 215 +++++++++++++++++++
 .../model/binRanges/AlphabeticVisualBinRange.ts    |  52 +++++
 .../model/binRanges/DateTimeVisualBinRange.ts      | 105 +++++++++
 .../model/binRanges/NominalVisualBinRange.ts       |  52 +++++
 .../model/binRanges/QuantitativeVisualBinRange.ts  |  90 ++++++++
 .../northstar/model/binRanges/VisualBinRange.ts    |  36 ++++
 .../model/binRanges/VisualBinRangeHelper.ts        |  71 +++++++
 src/client/northstar/operations/BaseOperation.ts   | 174 +++++++++++++++
 .../northstar/operations/HistogramOperation.ts     | 111 ++++++++++
 src/client/northstar/utils/ArrayUtil.ts            |  90 ++++++++
 src/client/northstar/utils/Extensions.ts           |  20 ++
 src/client/northstar/utils/GeometryUtil.ts         | 129 +++++++++++
 src/client/northstar/utils/IDisposable.ts          |   3 +
 src/client/northstar/utils/IEquatable.ts           |   3 +
 src/client/northstar/utils/KeyCodes.ts             | 137 ++++++++++++
 src/client/northstar/utils/MathUtil.ts             | 236 +++++++++++++++++++++
 src/client/northstar/utils/PartialClass.ts         |   7 +
 src/client/northstar/utils/Utils.ts                |  75 +++++++
 src/client/views/Main.tsx                          |  47 +++-
 src/client/views/nodes/DocumentContentsView.tsx    |   4 +-
 src/client/views/nodes/FieldView.tsx               |   7 +-
 src/client/views/nodes/HistogramBox.scss           |   8 +
 src/client/views/nodes/HistogramBox.tsx            |  67 ++++++
 src/fields/KeyStore.ts                             |   1 +
 40 files changed, 2250 insertions(+), 14 deletions(-)
 create mode 100644 src/client/northstar/core/BaseObject.ts
 create mode 100644 src/client/northstar/core/attribute/AttributeModel.ts
 create mode 100644 src/client/northstar/core/attribute/AttributeTransformationModel.ts
 create mode 100644 src/client/northstar/core/attribute/CalculatedAttributeModel.ts
 create mode 100644 src/client/northstar/core/brusher/BrushLinkModel.ts
 create mode 100644 src/client/northstar/core/brusher/IBaseBrushable.ts
 create mode 100644 src/client/northstar/core/brusher/IBaseBrusher.ts
 create mode 100644 src/client/northstar/core/filter/FilterModel.ts
 create mode 100644 src/client/northstar/core/filter/FilterOperand.ts
 create mode 100644 src/client/northstar/core/filter/FilterType.ts
 create mode 100644 src/client/northstar/core/filter/IBaseFilterConsumer.ts
 create mode 100644 src/client/northstar/core/filter/IBaseFilterProvider.ts
 create mode 100644 src/client/northstar/core/filter/ValueComparision.ts
 create mode 100644 src/client/northstar/model/ModelExtensions.ts
 create mode 100644 src/client/northstar/model/ModelHelpers.ts
 create mode 100644 src/client/northstar/model/binRanges/AlphabeticVisualBinRange.ts
 create mode 100644 src/client/northstar/model/binRanges/DateTimeVisualBinRange.ts
 create mode 100644 src/client/northstar/model/binRanges/NominalVisualBinRange.ts
 create mode 100644 src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts
 create mode 100644 src/client/northstar/model/binRanges/VisualBinRange.ts
 create mode 100644 src/client/northstar/model/binRanges/VisualBinRangeHelper.ts
 create mode 100644 src/client/northstar/operations/BaseOperation.ts
 create mode 100644 src/client/northstar/operations/HistogramOperation.ts
 create mode 100644 src/client/northstar/utils/ArrayUtil.ts
 create mode 100644 src/client/northstar/utils/Extensions.ts
 create mode 100644 src/client/northstar/utils/GeometryUtil.ts
 create mode 100644 src/client/northstar/utils/IDisposable.ts
 create mode 100644 src/client/northstar/utils/IEquatable.ts
 create mode 100644 src/client/northstar/utils/KeyCodes.ts
 create mode 100644 src/client/northstar/utils/MathUtil.ts
 create mode 100644 src/client/northstar/utils/PartialClass.ts
 create mode 100644 src/client/northstar/utils/Utils.ts
 create mode 100644 src/client/views/nodes/HistogramBox.scss
 create mode 100644 src/client/views/nodes/HistogramBox.tsx

(limited to 'src/client/views/nodes/FieldView.tsx')

diff --git a/package.json b/package.json
index c87e31bc4..33333a402 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
     "@types/connect-flash": "0.0.34",
     "@types/cookie-parser": "^1.4.1",
     "@types/cookie-session": "^2.0.36",
+    "@types/d3-format": "^1.3.1",
     "@types/express": "^4.16.1",
     "@types/express-flash": "0.0.0",
     "@types/express-session": "^1.15.12",
@@ -93,6 +94,7 @@
     "cookie-parser": "^1.4.4",
     "cookie-session": "^2.0.0-beta.3",
     "crypto-browserify": "^3.11.0",
+    "d3-format": "^1.3.2",
     "express": "^4.16.4",
     "express-flash": "0.0.2",
     "express-session": "^1.15.6",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4ed1fc3ed..b1e1f8c7b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -22,6 +22,7 @@ import { KeyValueBox } from "../views/nodes/KeyValueBox";
 import { PDFBox } from "../views/nodes/PDFBox";
 import { VideoBox } from "../views/nodes/VideoBox";
 import { WebBox } from "../views/nodes/WebBox";
+import { HistogramBox } from "../views/nodes/HistogramBox";
 
 export interface DocumentOptions {
     x?: number;
@@ -44,6 +45,7 @@ export interface DocumentOptions {
 
 export namespace Documents {
     let textProto: Document;
+    let histoProto: Document;
     let imageProto: Document;
     let webProto: Document;
     let collProto: Document;
@@ -52,6 +54,7 @@ export namespace Documents {
     let audioProto: Document;
     let pdfProto: Document;
     const textProtoId = "textProto";
+    const histoProtoId = "histoProto";
     const pdfProtoId = "pdfProto";
     const imageProtoId = "imageProto";
     const webProtoId = "webProto";
@@ -62,9 +65,10 @@ export namespace Documents {
 
     export function initProtos(callback: () => void) {
         Server.GetFields([collProtoId, textProtoId, imageProtoId], (fields) => {
+            textProto = fields[textProtoId] as Document;
+            histoProto = fields[histoProtoId] as Document;
             collProto = fields[collProtoId] as Document;
             imageProto = fields[imageProtoId] as Document;
-            textProto = fields[textProtoId] as Document;
             webProto = fields[webProtoId] as Document;
             kvpProto = fields[kvpProtoId] as Document;
             callback();
@@ -114,6 +118,11 @@ export namespace Documents {
         }
         return imageProto;
     }
+    function GetHistogramPrototype(): Document {
+        return histoProto ? histoProto :
+            histoProto = setupPrototypeOptions(textProtoId, "HISTO PROTOTYPE", HistogramBox.LayoutString(),
+                { x: 0, y: 0, width: 300, height: 300, layoutKeys: [KeyStore.Data] });
+    }
     function GetTextPrototype(): Document {
         return textProto ? textProto :
             textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
@@ -176,6 +185,9 @@ export namespace Documents {
         return assignToDelegate(SetInstanceOptions(GetAudioPrototype(), options, [new URL(url), AudioField]), options);
     }
 
+    export function HistogramDocument(options: DocumentOptions = {}) {
+        return assignToDelegate(SetInstanceOptions(GetHistogramPrototype(), options, ["", TextField]).MakeDelegate(), options);
+    }
     export function TextDocument(options: DocumentOptions = {}) {
         return assignToDelegate(SetInstanceOptions(GetTextPrototype(), options, ["", TextField]).MakeDelegate(), options);
     }
diff --git a/src/client/northstar/core/BaseObject.ts b/src/client/northstar/core/BaseObject.ts
new file mode 100644
index 000000000..e9e766e31
--- /dev/null
+++ b/src/client/northstar/core/BaseObject.ts
@@ -0,0 +1,12 @@
+import { IEquatable } from '../utils/IEquatable'
+import { IDisposable } from '../utils/IDisposable'
+
+export class BaseObject implements IEquatable, IDisposable {
+
+    public Equals(other: Object): boolean {
+        return this == other;
+    }
+
+    public Dispose(): void {
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/attribute/AttributeModel.ts b/src/client/northstar/core/attribute/AttributeModel.ts
new file mode 100644
index 000000000..124a5b45a
--- /dev/null
+++ b/src/client/northstar/core/attribute/AttributeModel.ts
@@ -0,0 +1,111 @@
+import { Attribute, DataType, VisualizationHint } from '../../model/idea/idea'
+import { BaseObject } from '../BaseObject'
+import { observable } from "mobx";
+
+export abstract class AttributeModel extends BaseObject {
+    public abstract get DisplayName(): string;
+    public abstract get CodeName(): string;
+    public abstract get DataType(): DataType;
+    public abstract get VisualizationHints(): VisualizationHint[];
+}
+
+export class ColumnAttributeModel extends AttributeModel {
+    public Attribute: Attribute;
+
+    constructor(attribute: Attribute) {
+        super();
+        this.Attribute = attribute;
+    }
+
+    public get DataType(): DataType {
+        return this.Attribute.dataType ? this.Attribute.dataType : DataType.Undefined;
+    }
+
+    public get DisplayName(): string {
+        return this.Attribute.displayName ? this.Attribute.displayName.ReplaceAll("_", " ") : "";
+    }
+
+    public get CodeName(): string {
+        return this.Attribute.rawName ? this.Attribute.rawName : "";
+    }
+
+    public get VisualizationHints(): VisualizationHint[] {
+        return this.Attribute.visualizationHints ? this.Attribute.visualizationHints : [];
+    }
+
+    public Equals(other: ColumnAttributeModel): boolean {
+        return this.Attribute.rawName == other.Attribute.rawName;
+    }
+}
+
+export class CodeAttributeModel extends AttributeModel {
+    private _visualizationHints: VisualizationHint[];
+
+    public CodeName: string;
+
+    @observable
+    public Code: string;
+
+    constructor(code: string, codeName: string, displayName: string, visualizationHints: VisualizationHint[]) {
+        super();
+        this.Code = code;
+        this.CodeName = codeName;
+        this.DisplayName = displayName;
+        this._visualizationHints = visualizationHints;
+    }
+
+    public get DataType(): DataType {
+        return DataType.Undefined;
+    }
+
+    @observable
+    public DisplayName: string;
+
+    public get VisualizationHints(): VisualizationHint[] {
+        return this._visualizationHints;
+    }
+
+    public Equals(other: CodeAttributeModel): boolean {
+        return this.CodeName === other.CodeName;
+    }
+
+}
+
+export class BackendAttributeModel extends AttributeModel {
+    private _dataType: DataType;
+    private _displayName: string;
+    private _codeName: string;
+    private _visualizationHints: VisualizationHint[];
+
+    public Id: string;
+
+    constructor(id: string, dataType: DataType, displayName: string, codeName: string, visualizationHints: VisualizationHint[]) {
+        super();
+        this.Id = id;
+        this._dataType = dataType;
+        this._displayName = displayName;
+        this._codeName = codeName;
+        this._visualizationHints = visualizationHints;
+    }
+
+    public get DataType(): DataType {
+        return this._dataType;
+    }
+
+    public get DisplayName(): string {
+        return this._displayName.ReplaceAll("_", " ");;
+    }
+
+    public get CodeName(): string {
+        return this._codeName;
+    }
+
+    public get VisualizationHints(): VisualizationHint[] {
+        return this._visualizationHints;
+    }
+
+    public Equals(other: BackendAttributeModel): boolean {
+        return this.Id == other.Id;
+    }
+
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/attribute/AttributeTransformationModel.ts b/src/client/northstar/core/attribute/AttributeTransformationModel.ts
new file mode 100644
index 000000000..cc5aa7154
--- /dev/null
+++ b/src/client/northstar/core/attribute/AttributeTransformationModel.ts
@@ -0,0 +1,47 @@
+;
+import { computed, observable } from "mobx";
+import { AggregateFunction } from "../../model/idea/idea";
+import { AttributeModel } from "./AttributeModel";
+import { IEquatable } from "../../utils/IEquatable";
+
+export class AttributeTransformationModel implements IEquatable {
+
+    @observable public AggregateFunction: AggregateFunction;
+    @observable public AttributeModel: AttributeModel;
+
+    constructor(attributeModel: AttributeModel, aggregateFunction: AggregateFunction = AggregateFunction.None) {
+        this.AttributeModel = attributeModel;
+        this.AggregateFunction = aggregateFunction;
+    }
+
+    @computed
+    public get PresentedName(): string {
+        var displayName = this.AttributeModel.DisplayName;
+        if (this.AggregateFunction === AggregateFunction.Count) {
+            return "count";
+        }
+        if (this.AggregateFunction === AggregateFunction.Avg)
+            displayName = "avg(" + displayName + ")";
+        else if (this.AggregateFunction === AggregateFunction.Max)
+            displayName = "max(" + displayName + ")";
+        else if (this.AggregateFunction === AggregateFunction.Min)
+            displayName = "min(" + displayName + ")";
+        else if (this.AggregateFunction === AggregateFunction.Sum)
+            displayName = "sum(" + displayName + ")";
+        else if (this.AggregateFunction === AggregateFunction.SumE)
+            displayName = "sumE(" + displayName + ")";
+
+        return displayName;
+    }
+
+    public clone(): AttributeTransformationModel {
+        var clone = new AttributeTransformationModel(this.AttributeModel);
+        clone.AggregateFunction = this.AggregateFunction;
+        return clone;
+    }
+
+    public Equals(other: AttributeTransformationModel): boolean {
+        return this.AggregateFunction == other.AggregateFunction &&
+            this.AttributeModel.Equals(other.AttributeModel);
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/attribute/CalculatedAttributeModel.ts b/src/client/northstar/core/attribute/CalculatedAttributeModel.ts
new file mode 100644
index 000000000..ab96c794d
--- /dev/null
+++ b/src/client/northstar/core/attribute/CalculatedAttributeModel.ts
@@ -0,0 +1,42 @@
+import { BackendAttributeModel, AttributeModel, CodeAttributeModel } from "./AttributeModel";
+import { DataType, VisualizationHint } from '../../model/idea/idea'
+
+export class CalculatedAttributeManager {
+    public static AllCalculatedAttributes: Array<AttributeModel> = new Array<AttributeModel>();
+
+    public static Clear() {
+        this.AllCalculatedAttributes = new Array<AttributeModel>();
+    }
+
+    public static CreateBackendAttributeModel(id: string, dataType: DataType, displayName: string, codeName: string, visualizationHints: VisualizationHint[]): BackendAttributeModel {
+        var filtered = this.AllCalculatedAttributes.filter(am => {
+            if (am instanceof BackendAttributeModel &&
+                am.Id == id) {
+                return true;
+            }
+            return false;
+        });
+        if (filtered.length > 0) {
+            return filtered[0] as BackendAttributeModel;
+        }
+        var newAttr = new BackendAttributeModel(id, dataType, displayName, codeName, visualizationHints);
+        this.AllCalculatedAttributes.push(newAttr);
+        return newAttr;
+    }
+
+    public static CreateCodeAttributeModel(code: string, codeName: string, visualizationHints: VisualizationHint[]): CodeAttributeModel {
+        var filtered = this.AllCalculatedAttributes.filter(am => {
+            if (am instanceof CodeAttributeModel &&
+                am.CodeName == codeName) {
+                return true;
+            }
+            return false;
+        });
+        if (filtered.length > 0) {
+            return filtered[0] as CodeAttributeModel;
+        }
+        var newAttr = new CodeAttributeModel(code, codeName, codeName.ReplaceAll("_", " "), visualizationHints);
+        this.AllCalculatedAttributes.push(newAttr);
+        return newAttr;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/brusher/BrushLinkModel.ts b/src/client/northstar/core/brusher/BrushLinkModel.ts
new file mode 100644
index 000000000..e3ac43367
--- /dev/null
+++ b/src/client/northstar/core/brusher/BrushLinkModel.ts
@@ -0,0 +1,40 @@
+import { IBaseBrushable } from '../brusher/IBaseBrushable'
+import { IBaseBrusher } from '../brusher/IBaseBrusher'
+import { Utils } from '../../utils/Utils'
+import { IEquatable } from '../../utils/IEquatable';
+
+export class BrushLinkModel<T> implements IEquatable {
+
+    public From: IBaseBrusher<T>;
+
+    public To: IBaseBrushable<T>;
+
+    public Color: number = 0;
+
+    constructor(from: IBaseBrusher<T>, to: IBaseBrushable<T>) {
+        this.From = from;
+        this.To = to;
+    }
+
+    public static overlaps(start: number, end: number, otherstart: number, otherend: number): boolean {
+        if (start > otherend || otherstart > end)
+            return false;
+        return true;
+    }
+    public static Connected<T>(from: IBaseBrusher<T>, to: IBaseBrushable<T>): boolean {
+        var connected = (Math.abs(from.Position.x + from.Size.x - to.Position.x) <= 60 &&
+            this.overlaps(from.Position.y, from.Position.y + from.Size.y, to.Position.y, to.Position.y + to.Size.y)
+        ) ||
+            (Math.abs(to.Position.x + to.Size.x - from.Position.x) <= 60 &&
+                this.overlaps(to.Position.y, to.Position.y + to.Size.y, from.Position.y, from.Position.y + from.Size.y)
+            );
+        return connected;
+    }
+
+    public Equals(other: Object): boolean {
+        if (!Utils.EqualityHelper(this, other)) return false;
+        if (!this.From.Equals((other as BrushLinkModel<T>).From)) return false;
+        if (!this.To.Equals((other as BrushLinkModel<T>).To)) return false;
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/brusher/IBaseBrushable.ts b/src/client/northstar/core/brusher/IBaseBrushable.ts
new file mode 100644
index 000000000..07d4e7580
--- /dev/null
+++ b/src/client/northstar/core/brusher/IBaseBrushable.ts
@@ -0,0 +1,13 @@
+import { BrushLinkModel } from '../brusher/BrushLinkModel'
+import { PIXIPoint } from '../../utils/MathUtil'
+import { IEquatable } from '../../utils/IEquatable';
+
+export interface IBaseBrushable<T> extends IEquatable {
+    BrusherModels: Array<BrushLinkModel<T>>;
+    BrushColors: Array<number>;
+    Position: PIXIPoint;
+    Size: PIXIPoint;
+}
+export function instanceOfIBaseBrushable<T>(object: any): object is IBaseBrushable<T> {
+    return 'BrusherModels' in object;
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/brusher/IBaseBrusher.ts b/src/client/northstar/core/brusher/IBaseBrusher.ts
new file mode 100644
index 000000000..d7ae65464
--- /dev/null
+++ b/src/client/northstar/core/brusher/IBaseBrusher.ts
@@ -0,0 +1,11 @@
+import { PIXIPoint } from '../../utils/MathUtil'
+import { IEquatable } from '../../utils/IEquatable';
+
+
+export interface IBaseBrusher<T> extends IEquatable {
+    Position: PIXIPoint;
+    Size: PIXIPoint;
+}
+export function instanceOfIBaseBrusher<T>(object: any): object is IBaseBrusher<T> {
+    return 'BrushableModels' in object;
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/filter/FilterModel.ts b/src/client/northstar/core/filter/FilterModel.ts
new file mode 100644
index 000000000..3c4cfc4a7
--- /dev/null
+++ b/src/client/northstar/core/filter/FilterModel.ts
@@ -0,0 +1,81 @@
+import { ValueComparison } from "./ValueComparision";
+import { Utils } from "../../utils/Utils";
+
+export class FilterModel {
+    public ValueComparisons: ValueComparison[];
+    constructor() {
+        this.ValueComparisons = new Array<ValueComparison>();
+    }
+
+    public Equals(other: FilterModel): boolean {
+        if (!Utils.EqualityHelper(this, other)) return false;
+        if (!this.isSame(this.ValueComparisons, (other as FilterModel).ValueComparisons)) return false;
+        return true;
+    }
+
+    private isSame(a: ValueComparison[], b: ValueComparison[]): boolean {
+        if (a.length !== b.length) {
+            return false;
+        }
+        for (let i = 0; i < a.length; i++) {
+            let valueComp = a[i];
+            if (!valueComp.Equals(b[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public ToPythonString(): string {
+        let ret = "(" + this.ValueComparisons.map(vc => vc.ToPythonString()).join("&&") + ")";
+        return ret;
+    }
+
+    public static And(filters: string[]): string {
+        let ret = filters.filter(f => f !== "").join(" && ");
+        return ret;
+    }
+
+    // public static GetFilterModelsRecursive(filterGraphNode: GraphNode<BaseOperationViewModel, FilterLinkViewModel>,
+    //     visitedFilterProviders: Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>, filterModels: FilterModel[], isFirst: boolean): string {
+    //     let ret = "";
+    //     if (Utils.isBaseFilterProvider(filterGraphNode.Data)) {
+    //         visitedFilterProviders.add(filterGraphNode);
+    //         let filtered = filterGraphNode.Data.FilterModels.filter(fm => fm && fm.ValueComparisons.length > 0);
+    //         if (!isFirst && filtered.length > 0) {
+    //             filterModels.push(...filtered);
+    //             ret = "(" + filterGraphNode.Data.FilterModels.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")";
+    //         }
+    //     }
+    //     if (Utils.isBaseFilterConsumer(filterGraphNode.Data) && filterGraphNode.Links != null) {
+    //         let children = new Array<string>();
+    //         let linkedGraphNodes = filterGraphNode.Links.get(LinkType.Filter);
+    //         if (linkedGraphNodes != null) {
+    //             for (let i = 0; i < linkedGraphNodes.length; i++) {
+    //                 let linkVm = linkedGraphNodes[i].Data;
+    //                 let linkedGraphNode = linkedGraphNodes[i].Target;
+    //                 if (!visitedFilterProviders.has(linkedGraphNode)) {
+    //                     let child = FilterModel.GetFilterModelsRecursive(linkedGraphNode, visitedFilterProviders, filterModels, false);
+    //                     if (child !== "") {
+    //                         if (linkVm.IsInverted) {
+    //                             child = "! " + child;
+    //                         }
+    //                         children.push(child);
+    //                     }
+    //                 }
+    //             }
+    //         }
+
+    //         let childrenJoined = children.join(filterGraphNode.Data.FilterOperand === FilterOperand.AND ? " && " : " || ");
+    //         if (children.length > 0) {
+    //             if (ret !== "") {
+    //                 ret = "(" + ret + " &&  (" + childrenJoined + "))";
+    //             }
+    //             else {
+    //                 ret = "(" + childrenJoined + ")";
+    //             }
+    //         }
+    //     }
+    //     return ret;
+    // }
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/filter/FilterOperand.ts b/src/client/northstar/core/filter/FilterOperand.ts
new file mode 100644
index 000000000..2e8e8d6a0
--- /dev/null
+++ b/src/client/northstar/core/filter/FilterOperand.ts
@@ -0,0 +1,5 @@
+export enum FilterOperand
+{
+    AND,
+    OR
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/filter/FilterType.ts b/src/client/northstar/core/filter/FilterType.ts
new file mode 100644
index 000000000..9adbc087f
--- /dev/null
+++ b/src/client/northstar/core/filter/FilterType.ts
@@ -0,0 +1,6 @@
+export enum FilterType
+{
+    Filter,
+    Brush,
+    Slice
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/filter/IBaseFilterConsumer.ts b/src/client/northstar/core/filter/IBaseFilterConsumer.ts
new file mode 100644
index 000000000..e687acb8a
--- /dev/null
+++ b/src/client/northstar/core/filter/IBaseFilterConsumer.ts
@@ -0,0 +1,10 @@
+import { FilterOperand } from '../filter/FilterOperand'
+import { IEquatable } from '../../utils/IEquatable'
+
+export interface IBaseFilterConsumer extends IEquatable {
+    FilterOperand: FilterOperand;
+}
+
+export function instanceOfIBaseFilterConsumer(object: any): object is IBaseFilterConsumer {
+    return 'FilterOperand' in object;
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/filter/IBaseFilterProvider.ts b/src/client/northstar/core/filter/IBaseFilterProvider.ts
new file mode 100644
index 000000000..d082bfe12
--- /dev/null
+++ b/src/client/northstar/core/filter/IBaseFilterProvider.ts
@@ -0,0 +1,8 @@
+import { FilterModel } from '../filter/FilterModel'
+
+export interface IBaseFilterProvider {
+    FilterModels: Array<FilterModel>;
+}
+export function instanceOfIBaseFilterProvider(object: any): object is IBaseFilterProvider {
+    return 'FilterModels' in object;
+}
\ No newline at end of file
diff --git a/src/client/northstar/core/filter/ValueComparision.ts b/src/client/northstar/core/filter/ValueComparision.ts
new file mode 100644
index 000000000..1e729d06e
--- /dev/null
+++ b/src/client/northstar/core/filter/ValueComparision.ts
@@ -0,0 +1,74 @@
+import { Predicate } from '../../model/idea/idea'
+import { Utils } from '../../utils/Utils'
+import { AttributeModel } from '../attribute/AttributeModel';
+
+export class ValueComparison {
+
+    public attributeModel: AttributeModel;
+    public Value: any;
+    public Predicate: Predicate;
+
+    public constructor(attributeModel: AttributeModel, predicate: Predicate, value: any) {
+        this.attributeModel = attributeModel;
+        this.Value = value;
+        this.Predicate = predicate;
+    }
+
+    public Equals(other: Object): boolean {
+        if (!Utils.EqualityHelper(this, other))
+            return false;
+        if (this.Predicate !== (other as ValueComparison).Predicate)
+            return false;
+        let isComplex = (typeof this.Value === "object");
+        if (!isComplex && this.Value != (other as ValueComparison).Value)
+            return false;
+        if (isComplex && !this.Value.Equals((other as ValueComparison).Value))
+            return false;
+        return true;
+    }
+
+    public ToPythonString(): string {
+        var op = "";
+        switch (this.Predicate) {
+            case Predicate.EQUALS:
+                op = "==";
+                break;
+            case Predicate.GREATER_THAN:
+                op = ">";
+                break;
+            case Predicate.GREATER_THAN_EQUAL:
+                op = ">=";
+                break;
+            case Predicate.LESS_THAN:
+                op = "<";
+                break;
+            case Predicate.LESS_THAN_EQUAL:
+                op = "<=";
+                break;
+            default:
+                op = "==";
+                break;
+        }
+
+        var val = this.Value.toString();
+        if (typeof this.Value === 'string' || this.Value instanceof String) {
+            val = "\"" + val + "\"";
+        }
+        var ret = " ";
+        var rawName = this.attributeModel.CodeName;
+        switch (this.Predicate) {
+            case Predicate.STARTS_WITH:
+                ret += rawName + " != null && " + rawName + ".StartsWith(" + val + ") ";
+                return ret;
+            case Predicate.ENDS_WITH:
+                ret += rawName + " != null && " + rawName + ".EndsWith(" + val + ") ";
+                return ret;
+            case Predicate.CONTAINS:
+                ret += rawName + " != null && " + rawName + ".Contains(" + val + ") ";
+                return ret;
+            default:
+                ret += rawName + " " + op + " " + val + " ";
+                return ret;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/model/ModelExtensions.ts b/src/client/northstar/model/ModelExtensions.ts
new file mode 100644
index 000000000..9fcba7f1c
--- /dev/null
+++ b/src/client/northstar/model/ModelExtensions.ts
@@ -0,0 +1,48 @@
+import { AttributeParameters, Brush, MarginAggregateParameters, SingleDimensionAggregateParameters, Solution } from '../model/idea/idea'
+import { Utils } from '../utils/Utils'
+
+import { FilterModel } from '../core/filter/FilterModel'
+
+(SingleDimensionAggregateParameters as any).prototype["Equals"] = function (other: Object) {
+    if (!Utils.EqualityHelper(this, other)) return false;
+    if (!Utils.EqualityHelper((this as SingleDimensionAggregateParameters).attributeParameters!,
+        (other as SingleDimensionAggregateParameters).attributeParameters!)) return false;
+    if (!((this as SingleDimensionAggregateParameters).attributeParameters! as any)["Equals"]((other as SingleDimensionAggregateParameters).attributeParameters)) return false;
+    return true;
+}
+
+{
+    (AttributeParameters as any).prototype["Equals"] = function (other: AttributeParameters) {
+        return (<any>this).constructor.name === (<any>other).constructor.name &&
+            this.rawName === other.rawName;
+    }
+}
+
+{
+    (Solution as any).prototype["Equals"] = function (other: Object) {
+        if (!Utils.EqualityHelper(this, other)) return false;
+        if ((this as Solution).solutionId !== (other as Solution).solutionId) return false;
+        return true;
+    }
+}
+
+{
+    (MarginAggregateParameters as any).prototype["Equals"] = function (other: Object) {
+        if (!Utils.EqualityHelper(this, other)) return false;
+        if (!Utils.EqualityHelper((this as SingleDimensionAggregateParameters).attributeParameters!,
+            (other as SingleDimensionAggregateParameters).attributeParameters!)) return false;
+        if (!((this as SingleDimensionAggregateParameters).attributeParameters! as any)["Equals"]((other as SingleDimensionAggregateParameters).attributeParameters!)) return false;
+
+        if ((this as MarginAggregateParameters).aggregateFunction !== (other as MarginAggregateParameters).aggregateFunction) return false;
+        return true;
+    }
+}
+
+{
+    (Brush as any).prototype["Equals"] = function (other: Object) {
+        if (!Utils.EqualityHelper(this, other)) return false;
+        if ((this as Brush).brushEnum !== (other as Brush).brushEnum) return false;
+        if ((this as Brush).brushIndex !== (other as Brush).brushIndex) return false;
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/model/ModelHelpers.ts b/src/client/northstar/model/ModelHelpers.ts
new file mode 100644
index 000000000..e1241b3ef
--- /dev/null
+++ b/src/client/northstar/model/ModelHelpers.ts
@@ -0,0 +1,215 @@
+
+import { action } from "mobx";
+import { AggregateFunction, AggregateKey, AggregateParameters, AttributeColumnParameters, AttributeParameters, AverageAggregateParameters, Bin, BinningParameters, Brush, BrushEnum, CountAggregateParameters, DataType, EquiWidthBinningParameters, HistogramResult, MarginAggregateParameters, SingleBinBinningParameters, SingleDimensionAggregateParameters, SumAggregateParameters, AggregateBinRange, NominalBinRange, AlphabeticBinRange, Predicate, Schema, Attribute, AttributeGroup, Exception, AttributeBackendParameters, AttributeCodeParameters } from '../model/idea/idea';
+import { ValueComparison } from "../core/filter/ValueComparision";
+import { ArrayUtil } from "../utils/ArrayUtil";
+import { AttributeModel, ColumnAttributeModel, BackendAttributeModel, CodeAttributeModel } from "../core/attribute/AttributeModel";
+import { FilterModel } from "../core/filter/FilterModel";
+import { AlphabeticVisualBinRange } from "./binRanges/AlphabeticVisualBinRange";
+import { NominalVisualBinRange } from "./binRanges/NominalVisualBinRange";
+import { VisualBinRangeHelper } from "./binRanges/VisualBinRangeHelper";
+import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
+import { Main } from "../../views/Main";
+
+export class ModelHelpers {
+
+    public static CreateAggregateKey(atm: AttributeTransformationModel, histogramResult: HistogramResult,
+        brushIndex: number, aggParameters?: SingleDimensionAggregateParameters): AggregateKey {
+        {
+            if (aggParameters == undefined) {
+                aggParameters = ModelHelpers.GetAggregateParameter(atm);
+            }
+            else {
+                aggParameters.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
+            }
+            return new AggregateKey(
+                {
+                    aggregateParameterIndex: ModelHelpers.GetAggregateParametersIndex(histogramResult, aggParameters),
+                    brushIndex: brushIndex
+                });
+        }
+    }
+
+    public static GetAggregateParametersIndex(histogramResult: HistogramResult, aggParameters?: AggregateParameters): number {
+        return ArrayUtil.IndexOfWithEqual(histogramResult.aggregateParameters!, aggParameters);
+    }
+
+    public static GetAggregateParameter(atm: AttributeTransformationModel): AggregateParameters | undefined {
+        var aggParam: AggregateParameters | undefined;
+        if (atm.AggregateFunction === AggregateFunction.Avg) {
+            var avg = new AverageAggregateParameters();
+            avg.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
+            avg.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+            aggParam = avg;
+        }
+        else if (atm.AggregateFunction === AggregateFunction.Count) {
+            var cnt = new CountAggregateParameters();
+            cnt.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
+            cnt.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+            aggParam = cnt;
+        }
+        else if (atm.AggregateFunction === AggregateFunction.Sum) {
+            var sum = new SumAggregateParameters();
+            sum.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
+            sum.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+            aggParam = sum;
+        }
+        return aggParam;
+    }
+
+    public static GetAggregateParametersWithMargins(atms: Array<AttributeTransformationModel>): Array<AggregateParameters> {
+        var aggregateParameters = new Array<AggregateParameters>();
+        atms.forEach(agg => {
+            var aggParams = ModelHelpers.GetAggregateParameter(agg);
+            if (aggParams) {
+                aggregateParameters.push(aggParams);
+
+                var margin = new MarginAggregateParameters();
+                margin.attributeParameters = ModelHelpers.GetAttributeParameters(agg.AttributeModel);
+                margin.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+                margin.aggregateFunction = agg.AggregateFunction;
+                aggregateParameters.push(margin);
+            }
+        });
+
+        return aggregateParameters;
+    }
+
+    public static GetBinningParameters(attr: AttributeTransformationModel, nrOfBins: number, minvalue?: number, maxvalue?: number): BinningParameters {
+        if (attr.AggregateFunction === AggregateFunction.None) {
+            return new EquiWidthBinningParameters(
+                {
+                    attributeParameters: ModelHelpers.GetAttributeParameters(attr.AttributeModel),
+                    requestedNrOfBins: nrOfBins,
+                    minValue: minvalue,
+                    maxValue: maxvalue
+                });
+        }
+        else {
+            return new SingleBinBinningParameters(
+                {
+                    attributeParameters: ModelHelpers.GetAttributeParameters(attr.AttributeModel)
+                });
+        }
+    }
+
+    public static GetAttributeParametersFromAttributeModel(am: AttributeModel): AttributeParameters {
+        if (am instanceof ColumnAttributeModel) {
+            return new AttributeColumnParameters(
+                {
+                    rawName: am.CodeName,
+                    visualizationHints: am.VisualizationHints
+                });
+        }
+        else if (am instanceof BackendAttributeModel) {
+            return new AttributeBackendParameters(
+                {
+                    rawName: am.CodeName,
+                    visualizationHints: am.VisualizationHints,
+                    id: (am as BackendAttributeModel).Id
+                });
+        }
+        else if (am instanceof CodeAttributeModel) {
+            return new AttributeCodeParameters(
+                {
+                    rawName: am.CodeName,
+                    visualizationHints: am.VisualizationHints,
+                    code: (am as CodeAttributeModel).Code
+                });
+        }
+        else {
+            throw new Exception()
+        }
+    }
+
+    public static GetAttributeParameters(am: AttributeModel): AttributeParameters {
+        return this.GetAttributeParametersFromAttributeModel(am);
+    }
+
+    public static OverlapBrushIndex(histogramResult: HistogramResult): number {
+        var brush = ArrayUtil.First(histogramResult.brushes!, (b: any) => b.brushEnum === BrushEnum.Overlap);
+        return ModelHelpers.GetBrushIndex(histogramResult, brush);
+    }
+
+    public static AllBrushIndex(histogramResult: HistogramResult): number {
+        var brush = ArrayUtil.First(histogramResult.brushes!, (b: any) => b.brushEnum === BrushEnum.All);
+        return ModelHelpers.GetBrushIndex(histogramResult, brush);
+    }
+
+    public static RestBrushIndex(histogramResult: HistogramResult): number {
+        var brush = ArrayUtil.First(histogramResult.brushes!, (b: Brush) => b.brushEnum === BrushEnum.Rest);
+        return ModelHelpers.GetBrushIndex(histogramResult, brush);
+    }
+
+    public static GetBrushIndex(histogramResult: HistogramResult, brush: Brush): number {
+        return ArrayUtil.IndexOfWithEqual(histogramResult.brushes!, brush);
+    }
+
+    public static GetAggregateResult(bin: Bin, aggregateKey: AggregateKey) {
+        if (aggregateKey.aggregateParameterIndex == -1 || aggregateKey.brushIndex == -1) {
+            return null;
+        }
+        return bin.aggregateResults![aggregateKey.aggregateParameterIndex! * bin.ySize! + aggregateKey.brushIndex!];
+    }
+
+    @action
+    public static PossibleAggegationFunctions(atm: AttributeTransformationModel): Array<AggregateFunction> {
+        var ret = new Array<AggregateFunction>();
+        ret.push(AggregateFunction.None);
+        ret.push(AggregateFunction.Count);
+        if (atm.AttributeModel.DataType == DataType.Float ||
+            atm.AttributeModel.DataType == DataType.Double ||
+            atm.AttributeModel.DataType == DataType.Int) {
+            ret.push(AggregateFunction.Avg);
+            ret.push(AggregateFunction.Sum);
+        }
+        return ret;
+    }
+
+    public static GetBinFilterModel(
+        bin: Bin, brushIndex: number, histogramResult: HistogramResult,
+        xAom: AttributeTransformationModel, yAom: AttributeTransformationModel): FilterModel {
+        var dimensions: Array<AttributeTransformationModel> = [xAom, yAom];
+        var filterModel = new FilterModel();
+
+        for (var i = 0; i < histogramResult.binRanges!.length; i++) {
+            if (!(histogramResult.binRanges![i] instanceof AggregateBinRange)) {
+                var binRange = VisualBinRangeHelper.GetNonAggregateVisualBinRange(histogramResult.binRanges![i]);
+                var dataFrom = binRange.GetValueFromIndex(bin.binIndex!.indices![i]);
+                var dataTo = binRange.AddStep(dataFrom);
+
+                if (binRange instanceof NominalVisualBinRange) {
+                    var tt = binRange.GetLabel(dataFrom);
+                    filterModel.ValueComparisons.push(new ValueComparison(dimensions[i].AttributeModel, Predicate.EQUALS, tt));
+                }
+                else if (binRange instanceof AlphabeticVisualBinRange) {
+                    filterModel.ValueComparisons.push(new ValueComparison(dimensions[i].AttributeModel, Predicate.STARTS_WITH,
+                        binRange.GetLabel(dataFrom)));
+                }
+                else {
+                    filterModel.ValueComparisons.push(new ValueComparison(dimensions[i].AttributeModel, Predicate.GREATER_THAN_EQUAL, dataFrom));
+                    filterModel.ValueComparisons.push(new ValueComparison(dimensions[i].AttributeModel, Predicate.LESS_THAN, dataTo));
+                }
+            }
+        }
+
+        return filterModel;
+    }
+
+    public GetAllAttributes(schema: Schema) {
+        if (!schema || !schema.rootAttributeGroup) {
+            return [];
+        }
+        const recurs = (attrs: Attribute[], g: AttributeGroup) => {
+            if (g.attributes) {
+                attrs.push.apply(attrs, g.attributes);
+                if (g.attributeGroups) {
+                    g.attributeGroups.forEach(ng => recurs(attrs, ng));
+                }
+            }
+        };
+        const allAttributes: Attribute[] = new Array<Attribute>();
+        recurs(allAttributes, schema.rootAttributeGroup);
+        return allAttributes;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/AlphabeticVisualBinRange.ts b/src/client/northstar/model/binRanges/AlphabeticVisualBinRange.ts
new file mode 100644
index 000000000..995bf4e0b
--- /dev/null
+++ b/src/client/northstar/model/binRanges/AlphabeticVisualBinRange.ts
@@ -0,0 +1,52 @@
+import { AlphabeticBinRange, BinLabel } from '../../model/idea/idea'
+import { VisualBinRange } from './VisualBinRange'
+
+export class AlphabeticVisualBinRange extends VisualBinRange {
+    public DataBinRange: AlphabeticBinRange;
+
+    constructor(dataBinRange: AlphabeticBinRange) {
+        super();
+        this.DataBinRange = dataBinRange;
+    }
+
+    public AddStep(value: number): number {
+        return value + 1;
+    }
+
+    public GetValueFromIndex(index: number): number {
+        return index;
+    }
+
+    public GetBins(): number[] {
+        var bins = new Array<number>();
+        var idx = 0;
+        for (var key in this.DataBinRange.labelsValue) {
+            if (this.DataBinRange.labelsValue.hasOwnProperty(key)) {
+                bins.push(idx);
+                idx++;
+            }
+        }
+        return bins;
+    }
+
+    public GetLabel(value: number): string {
+        return this.DataBinRange.prefix + this.DataBinRange.valuesLabel![value];
+    }
+
+    public GetLabels(): Array<BinLabel> {
+        var labels = new Array<BinLabel>();
+        var count = 0;
+        for (var key in this.DataBinRange.valuesLabel) {
+            if (this.DataBinRange.valuesLabel.hasOwnProperty(key)) {
+                var value = this.DataBinRange.valuesLabel[key];
+                labels.push(new BinLabel({
+                    value: parseFloat(key),
+                    minValue: count++,
+                    maxValue: count,
+                    label: this.DataBinRange.prefix + value
+                }));
+            }
+        }
+        return labels;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/DateTimeVisualBinRange.ts b/src/client/northstar/model/binRanges/DateTimeVisualBinRange.ts
new file mode 100644
index 000000000..9313fb1a7
--- /dev/null
+++ b/src/client/northstar/model/binRanges/DateTimeVisualBinRange.ts
@@ -0,0 +1,105 @@
+import { DateTimeBinRange, DateTimeStep, DateTimeStepGranularity } from '../idea/idea'
+import { VisualBinRange } from './VisualBinRange'
+
+export class DateTimeVisualBinRange extends VisualBinRange {
+    public DataBinRange: DateTimeBinRange;
+
+    constructor(dataBinRange: DateTimeBinRange) {
+        super();
+        this.DataBinRange = dataBinRange;
+    }
+
+    public AddStep(value: number): number {
+        return DateTimeVisualBinRange.AddToDateTimeTicks(value, this.DataBinRange.step!);
+    }
+
+    public GetValueFromIndex(index: number): number {
+        var v = this.DataBinRange.minValue!;
+        for (var i = 0; i < index; i++) {
+            v = this.AddStep(v);
+        }
+        return v;
+    }
+
+    public GetBins(): number[] {
+        var bins = new Array<number>();
+        for (var v: number = this.DataBinRange.minValue!;
+            v < this.DataBinRange.maxValue!;
+            v = DateTimeVisualBinRange.AddToDateTimeTicks(v, this.DataBinRange.step!)) {
+            bins.push(v);
+        }
+        return bins;
+    }
+
+    private pad(n: number, size: number) {
+        var sign = n < 0 ? '-' : '';
+        return sign + new Array(size).concat([Math.abs(n)]).join('0').slice(-size);
+    }
+
+
+    public GetLabel(value: number): string {
+        var dt = DateTimeVisualBinRange.TicksToDate(value);
+        if (this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Second ||
+            this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Minute) {
+            return ("" + this.pad(dt.getMinutes(), 2) + ":" + this.pad(dt.getSeconds(), 2));
+            //return dt.ToString("mm:ss");
+        }
+        else if (this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Hour) {
+            return (this.pad(dt.getHours(), 2) + ":" + this.pad(dt.getMinutes(), 2));
+            //return dt.ToString("HH:mm");
+        }
+        else if (this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Day) {
+            return ((dt.getMonth() + 1) + "/" + dt.getDate() + "/" + dt.getFullYear());
+            //return dt.ToString("MM/dd/yyyy");
+        }
+        else if (this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Month) {
+            //return dt.ToString("MM/yyyy");
+            return ((dt.getMonth() + 1) + "/" + dt.getFullYear());
+        }
+        else if (this.DataBinRange.step!.dateTimeStepGranularity == DateTimeStepGranularity.Year) {
+            return "" + dt.getFullYear();
+        }
+        return "n/a";
+    }
+
+    public static TicksToDate(ticks: number): Date {
+        var dd = new Date((ticks - 621355968000000000) / 10000);
+        dd.setMinutes(dd.getMinutes() + dd.getTimezoneOffset());
+        return dd;
+    }
+
+
+    public static DateToTicks(date: Date): number {
+        var copiedDate = new Date(date.getTime());
+        copiedDate.setMinutes(copiedDate.getMinutes() - copiedDate.getTimezoneOffset());
+        var t = copiedDate.getTime() * 10000 + 621355968000000000;
+        /*var dd = new Date((ticks - 621355968000000000) / 10000);
+        dd.setMinutes(dd.getMinutes() + dd.getTimezoneOffset());
+        return dd;*/
+        return t;
+    }
+
+    public static AddToDateTimeTicks(ticks: number, dateTimeStep: DateTimeStep): number {
+        var copiedDate = DateTimeVisualBinRange.TicksToDate(ticks);
+        var returnDate: Date = new Date(Date.now());
+        if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Second) {
+            returnDate = new Date(copiedDate.setSeconds(copiedDate.getSeconds() + dateTimeStep.dateTimeStepValue!));
+        }
+        else if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Minute) {
+            returnDate = new Date(copiedDate.setMinutes(copiedDate.getMinutes() + dateTimeStep.dateTimeStepValue!));
+        }
+        else if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Hour) {
+            returnDate = new Date(copiedDate.setHours(copiedDate.getHours() + dateTimeStep.dateTimeStepValue!));
+        }
+        else if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Day) {
+            returnDate = new Date(copiedDate.setDate(copiedDate.getDate() + dateTimeStep.dateTimeStepValue!));
+        }
+        else if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Month) {
+            returnDate = new Date(copiedDate.setMonth(copiedDate.getMonth() + dateTimeStep.dateTimeStepValue!));
+        }
+        else if (dateTimeStep.dateTimeStepGranularity == DateTimeStepGranularity.Year) {
+            returnDate = new Date(copiedDate.setFullYear(copiedDate.getFullYear() + dateTimeStep.dateTimeStepValue!));
+        }
+        return DateTimeVisualBinRange.DateToTicks(returnDate);
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/NominalVisualBinRange.ts b/src/client/northstar/model/binRanges/NominalVisualBinRange.ts
new file mode 100644
index 000000000..407ff3ea6
--- /dev/null
+++ b/src/client/northstar/model/binRanges/NominalVisualBinRange.ts
@@ -0,0 +1,52 @@
+import { NominalBinRange, BinLabel } from '../../model/idea/idea'
+import { VisualBinRange } from './VisualBinRange'
+
+export class NominalVisualBinRange extends VisualBinRange {
+    public DataBinRange: NominalBinRange;
+
+    constructor(dataBinRange: NominalBinRange) {
+        super();
+        this.DataBinRange = dataBinRange;
+    }
+
+    public AddStep(value: number): number {
+        return value + 1;
+    }
+
+    public GetValueFromIndex(index: number): number {
+        return index;
+    }
+
+    public GetBins(): number[] {
+        var bins = new Array<number>();
+        var idx = 0;
+        for (var key in this.DataBinRange.labelsValue) {
+            if (this.DataBinRange.labelsValue.hasOwnProperty(key)) {
+                bins.push(idx);
+                idx++;
+            }
+        }
+        return bins;
+    }
+
+    public GetLabel(value: number): string {
+        return this.DataBinRange.valuesLabel![value];
+    }
+
+    public GetLabels(): Array<BinLabel> {
+        var labels = new Array<BinLabel>();
+        var count = 0;
+        for (var key in this.DataBinRange.valuesLabel) {
+            if (this.DataBinRange.valuesLabel.hasOwnProperty(key)) {
+                var value = this.DataBinRange.valuesLabel[key];
+                labels.push(new BinLabel({
+                    value: parseFloat(key),
+                    minValue: count++,
+                    maxValue: count,
+                    label: value
+                }));
+            }
+        }
+        return labels;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts b/src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts
new file mode 100644
index 000000000..80886416b
--- /dev/null
+++ b/src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts
@@ -0,0 +1,90 @@
+import { QuantitativeBinRange } from '../idea/idea'
+import { VisualBinRange } from './VisualBinRange';
+import { format } from "d3-format";
+
+export class QuantitativeVisualBinRange extends VisualBinRange {
+
+    public DataBinRange: QuantitativeBinRange;
+
+    constructor(dataBinRange: QuantitativeBinRange) {
+        super();
+        this.DataBinRange = dataBinRange;
+    }
+
+    public AddStep(value: number): number {
+        return value + this.DataBinRange.step!;
+    }
+
+    public GetValueFromIndex(index: number): number {
+        return this.DataBinRange.minValue! + (index * this.DataBinRange.step!);
+    }
+
+    public GetLabel(value: number): string {
+        return QuantitativeVisualBinRange.NumberFormatter(value);
+    }
+
+    public static NumberFormatter(val: number): string {
+        if (val === 0) {
+            return "0";
+        }
+        if (val < 1) {
+            /*if (val < Math.abs(0.001))  {
+                return val.toExponential(2);
+            }*/
+            return format(".3")(val);
+        }
+        return format("~s")(val);
+    }
+
+    public GetBins(): number[] {
+        let bins = new Array<number>();
+
+        for (let v: number = this.DataBinRange.minValue!; v < this.DataBinRange.maxValue!; v += this.DataBinRange.step!) {
+            bins.push(v);
+        }
+        return bins;
+    }
+
+    public static Initialize(dataMinValue: number, dataMaxValue: number, targetBinNumber: number, isIntegerRange: boolean): QuantitativeVisualBinRange {
+        let extent = QuantitativeVisualBinRange.getExtent(dataMinValue, dataMaxValue, targetBinNumber, isIntegerRange);
+        let dataBinRange = new QuantitativeBinRange();
+        dataBinRange.minValue = extent[0];
+        dataBinRange.maxValue = extent[1];
+        dataBinRange.step = extent[2];
+
+        return new QuantitativeVisualBinRange(dataBinRange);
+    }
+
+    private static getExtent(dataMin: number, dataMax: number, m: number, isIntegerRange: boolean): number[] {
+        if (dataMin === dataMax) {
+            // dataMin -= 0.1;
+            dataMax += 0.1;
+        }
+        let span = dataMax - dataMin;
+
+        let step = Math.pow(10, Math.floor(Math.log10(span / m)));
+        let err = m / span * step;
+
+        if (err <= .15) {
+            step *= 10;
+        }
+        else if (err <= .35) {
+            step *= 5;
+        }
+        else if (err <= .75) {
+            step *= 2;
+        }
+
+        if (isIntegerRange) {
+            step = Math.ceil(step);
+        }
+        let ret: number[] = new Array<number>(3);
+        let minDivStep = Math.floor(dataMin / step);
+        let maxDivStep = Math.floor(dataMax / step);
+        ret[0] = minDivStep * step; // Math.floor(Math.Round(dataMin, 8)/step)*step;
+        ret[1] = maxDivStep * step + step; // Math.floor(Math.Round(dataMax, 8)/step)*step + step;
+        ret[2] = step;
+
+        return ret;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/VisualBinRange.ts b/src/client/northstar/model/binRanges/VisualBinRange.ts
new file mode 100644
index 000000000..f53008f9a
--- /dev/null
+++ b/src/client/northstar/model/binRanges/VisualBinRange.ts
@@ -0,0 +1,36 @@
+import { BinLabel } from '../../model/idea/idea'
+
+export abstract class VisualBinRange {
+
+    constructor() {
+
+    }
+
+    public abstract AddStep(value: number): number;
+
+    public abstract GetValueFromIndex(index: number): number;
+
+    public abstract GetBins(): Array<number>;
+
+    public GetLabel(value: number): string {
+        return value.toString();
+    }
+
+    public GetLabels(): Array<BinLabel> {
+        var labels = new Array<BinLabel>();
+        var bins = this.GetBins();
+        bins.forEach(b => {
+            labels.push(new BinLabel({
+                value: b,
+                minValue: b,
+                maxValue: this.AddStep(b),
+                label: this.GetLabel(b)
+            }));
+        });
+        return labels;
+    }
+}
+
+export enum ChartType {
+    HorizontalBar = 0, VerticalBar = 1, HeatMap = 2, SinglePoint = 3
+}
\ No newline at end of file
diff --git a/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts
new file mode 100644
index 000000000..9eae39800
--- /dev/null
+++ b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts
@@ -0,0 +1,71 @@
+import { BinRange, NominalBinRange, QuantitativeBinRange, Exception, AlphabeticBinRange, DateTimeBinRange, AggregateBinRange, DoubleValueAggregateResult, HistogramResult } from "../idea/idea";
+import { VisualBinRange, ChartType } from "./VisualBinRange";
+import { NominalVisualBinRange } from "./NominalVisualBinRange";
+import { QuantitativeVisualBinRange } from "./QuantitativeVisualBinRange";
+import { AlphabeticVisualBinRange } from "./AlphabeticVisualBinRange";
+import { DateTimeVisualBinRange } from "./DateTimeVisualBinRange";
+import { Settings } from "../../manager/Gateway";
+import { ModelHelpers } from "../ModelHelpers";
+import { AttributeTransformationModel } from "../../core/attribute/AttributeTransformationModel";
+
+export const SETTINGS_X_BINS = 15;
+export const SETTINGS_Y_BINS = 15;
+export const SETTINGS_SAMPLE_SIZE = 100000;
+
+export class VisualBinRangeHelper {
+
+    public static GetNonAggregateVisualBinRange(dataBinRange: BinRange): VisualBinRange {
+        if (dataBinRange instanceof NominalBinRange) {
+            return new NominalVisualBinRange(dataBinRange as NominalBinRange);
+        }
+        else if (dataBinRange instanceof QuantitativeBinRange) {
+            return new QuantitativeVisualBinRange(dataBinRange as QuantitativeBinRange);
+        }
+        else if (dataBinRange instanceof AlphabeticBinRange) {
+            return new AlphabeticVisualBinRange(dataBinRange as AlphabeticBinRange);
+        }
+        else if (dataBinRange instanceof DateTimeBinRange) {
+            return new DateTimeVisualBinRange(dataBinRange as DateTimeBinRange);
+        }
+        throw new Exception()
+    }
+
+    public static GetVisualBinRange(dataBinRange: BinRange, histoResult: HistogramResult, attr: AttributeTransformationModel, chartType: ChartType): VisualBinRange {
+
+        if (!(dataBinRange instanceof AggregateBinRange)) {
+            return VisualBinRangeHelper.GetNonAggregateVisualBinRange(dataBinRange);
+        }
+        else {
+            var aggregateKey = ModelHelpers.CreateAggregateKey(attr, histoResult, ModelHelpers.AllBrushIndex(histoResult));
+            var minValue = Number.MAX_VALUE;
+            var maxValue = Number.MIN_VALUE;
+            for (var b = 0; b < histoResult.brushes!.length; b++) {
+                var brush = histoResult.brushes![b];
+                aggregateKey.brushIndex = brush.brushIndex;
+                for (var key in histoResult.bins) {
+                    if (histoResult.bins.hasOwnProperty(key)) {
+                        var bin = histoResult.bins[key];
+                        var res = <DoubleValueAggregateResult>ModelHelpers.GetAggregateResult(bin, aggregateKey);
+                        if (res && res.hasResult && res.result) {
+                            minValue = Math.min(minValue, res.result);
+                            maxValue = Math.max(maxValue, res.result);
+                        }
+                    }
+                }
+            };
+
+            let visualBinRange = QuantitativeVisualBinRange.Initialize(minValue, maxValue, 10, false);
+
+            if (chartType == ChartType.HorizontalBar || chartType == ChartType.VerticalBar) {
+                visualBinRange = QuantitativeVisualBinRange.Initialize(Math.min(0, minValue),
+                    Math.max(0, (visualBinRange as QuantitativeVisualBinRange).DataBinRange.maxValue!),
+                    SETTINGS_X_BINS, false);
+            }
+            else if (chartType == ChartType.SinglePoint) {
+                visualBinRange = QuantitativeVisualBinRange.Initialize(Math.min(0, minValue), Math.max(0, maxValue),
+                    SETTINGS_X_BINS, false);
+            }
+            return visualBinRange;
+        }
+    }
+}
diff --git a/src/client/northstar/operations/BaseOperation.ts b/src/client/northstar/operations/BaseOperation.ts
new file mode 100644
index 000000000..4c0303a48
--- /dev/null
+++ b/src/client/northstar/operations/BaseOperation.ts
@@ -0,0 +1,174 @@
+import { FilterModel } from '../core/filter/FilterModel'
+import { ErrorResult, Exception, OperationParameters, OperationReference, Result, ResultParameters } from '../model/idea/idea';
+import { action, computed, observable } from "mobx";
+import { Gateway } from '../manager/Gateway';
+
+export abstract class BaseOperation {
+    private _interactionTimeoutId: number = 0;
+    private static _currentOperations: Map<number, PollPromise> = new Map<number, PollPromise>();
+    //public InteractionTimeout: EventDelegate<InteractionTimeoutEventArgs> = new EventDelegate<InteractionTimeoutEventArgs>();
+
+    @observable public Error: string = "";
+    @observable public OverridingFilters: FilterModel[] = [];
+    @observable public Result: Result | undefined;
+    @observable public ComputationStarted: boolean = false;
+    public OperationReference: OperationReference | undefined = undefined;
+
+    private static _nextId = 0;
+    public RequestSalt: string = "";
+    public Id: number;
+
+    constructor() {
+        this.Id = BaseOperation._nextId++;
+    }
+
+    @computed
+    public get FilterString(): string {
+
+        // if (this.OverridingFilters.length > 0) {
+        //     return "(" + this.OverridingFilters.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")";
+        // }
+        // let rdg = MainManager.Instance.MainViewModel.FilterReverseDependencyGraph;
+        // let sliceModel = this.TypedViewModel.IncomingSliceModel;
+        // if (sliceModel != null && sliceModel.Source != null && instanceOfIBaseFilterProvider(sliceModel.Source) && rdg.has(sliceModel.Source)) {
+        //     let filterModels = sliceModel.Source.FilterModels.map(f => f);
+        //     return FilterModel.GetFilterModelsRecursive(rdg.get(sliceModel.Source), new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, false);
+        // }
+
+        // if (rdg.has(this.TypedViewModel)) {
+        //     let filterModels = [];
+        //     return FilterModel.GetFilterModelsRecursive(rdg.get(this.TypedViewModel), new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, true)
+        // }
+        return "";
+    }
+
+
+    @action
+    public SetResult(result: Result): void {
+        this.Result = result;
+    }
+
+    public async Update(): Promise<void> {
+
+        try {
+            if (BaseOperation._currentOperations.has(this.Id)) {
+                BaseOperation._currentOperations.get(this.Id)!.Cancel();
+                if (this.OperationReference) {
+                    Gateway.Instance.PauseOperation(this.OperationReference.toJSON());
+                }
+            }
+
+            let operationParameters = this.CreateOperationParameters();
+            this.Result = undefined;
+            this.Error = "";
+            let salt = Math.random().toString();
+            this.RequestSalt = salt;
+
+            if (!operationParameters) {
+                this.ComputationStarted = false;
+                return;
+            }
+
+            this.ComputationStarted = true;
+            //let start = performance.now();
+            let promise = Gateway.Instance.StartOperation(operationParameters.toJSON());
+            promise.catch(err => {
+                action(() => {
+                    this.Error = err;
+                    console.error(err);
+                });
+            });
+            let operationReference = await promise;
+
+
+            if (operationReference) {
+                this.OperationReference = operationReference;
+
+                let resultParameters = new ResultParameters();
+                resultParameters.operationReference = operationReference;
+
+                let pollPromise = new PollPromise(salt, operationReference);
+                BaseOperation._currentOperations.set(this.Id, pollPromise);
+
+                pollPromise.Start(async () => {
+                    let result = await Gateway.Instance.GetResult(resultParameters.toJSON());
+                    if (result instanceof ErrorResult) {
+                        throw new Error((result as ErrorResult).message);
+                    }
+                    if (this.RequestSalt == pollPromise.RequestSalt) {
+                        if (result && (!this.Result || this.Result.progress != result.progress)) {
+                            /*if (operationViewModel.Result !== null && operationViewModel.Result !== undefined) {
+                                let t1 = performance.now();
+                                console.log((t1 - start) + " milliseconds.");
+                                start = performance.now();
+                            }*/
+                            this.SetResult(result);
+                        }
+
+                        if (!result || result.progress! < 1) {
+                            return true;
+                        }
+                    }
+                    return false;
+                }, 100).catch((err: Error) => action(() => {
+                    this.Error = err.message;
+                    console.error(err.message);
+                })()
+                );
+            }
+        }
+        catch (err) {
+            console.error(err as Exception);
+            // ErrorDialog.Instance.HandleError(err, operationViewModel);
+        }
+    }
+
+    public CreateOperationParameters(): OperationParameters | undefined { return undefined; }
+
+    private interactionTimeout() {
+        // clearTimeout(this._interactionTimeoutId);
+        // this.InteractionTimeout.Fire(new InteractionTimeoutEventArgs(this.TypedViewModel, InteractionTimeoutType.Timeout));
+    }
+}
+
+export class PollPromise {
+    public RequestSalt: string;
+    public OperationReference: OperationReference;
+
+    private _notCanceled: boolean = true;
+    private _poll: undefined | (() => Promise<boolean>);
+    private _delay: number = 0;
+
+    public constructor(requestKey: string, operationReference: OperationReference) {
+        this.RequestSalt = requestKey;
+        this.OperationReference = operationReference;
+    }
+
+    public Cancel(): void {
+        this._notCanceled = false;
+    }
+
+    public Start(poll: () => Promise<boolean>, delay: number): Promise<void> {
+        this._poll = poll;
+        this._delay = delay;
+        return this.pollRecursive();
+    }
+
+    private pollRecursive = (): Promise<void> => {
+        return Promise.resolve().then(this._poll).then((flag) => {
+            this._notCanceled && flag && new Promise((res) => (setTimeout(res, this._delay)))
+                .then(this.pollRecursive);
+        });
+    }
+}
+
+
+export class InteractionTimeoutEventArgs {
+    constructor(public Sender: object, public Type: InteractionTimeoutType) {
+    }
+}
+
+export enum InteractionTimeoutType {
+    Reset = 0,
+    Timeout = 1
+}
diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
new file mode 100644
index 000000000..a4f5cac70
--- /dev/null
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -0,0 +1,111 @@
+import { reaction, computed, action } from "mobx";
+import { Attribute, DataType, QuantitativeBinRange, HistogramOperationParameters, AggregateParameters, AggregateFunction, AverageAggregateParameters } from "../model/idea/idea";
+import { ArrayUtil } from "../utils/ArrayUtil";
+import { CalculatedAttributeManager } from "../core/attribute/CalculatedAttributeModel";
+import { ModelHelpers } from "../model/ModelHelpers";
+import { SETTINGS_X_BINS, SETTINGS_Y_BINS, SETTINGS_SAMPLE_SIZE } from "../model/binRanges/VisualBinRangeHelper";
+import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
+import { Main } from "../../views/Main";
+import { BaseOperation } from "./BaseOperation";
+
+
+export class HistogramOperation extends BaseOperation {
+    public X: AttributeTransformationModel;
+    public Y: AttributeTransformationModel;
+    public V: AttributeTransformationModel;
+    constructor(x: AttributeTransformationModel, y: AttributeTransformationModel, v: AttributeTransformationModel) {
+        super();
+        this.X = x;
+        this.Y = y;
+        this.V = v;
+        reaction(() => this.createOperationParamsCache, () => this.Update());
+    }
+
+    @computed.struct
+    public get BrushString() {
+        return [];
+        // let brushes = [];
+        // this.TypedViewModel.BrusherModels.map(brushLinkModel => {
+        //     if (instanceOfIBaseFilterProvider(brushLinkModel.From) && brushLinkModel.From.FilterModels.some && brushLinkModel.From instanceof BaseOperationViewModel) {
+        //         let brushFilterModels = [];
+        //         let gnode = MainManager.Instance.MainViewModel.FilterDependencyGraph.has(brushLinkModel.From) ?
+        //             MainManager.Instance.MainViewModel.FilterDependencyGraph.get(brushLinkModel.From) :
+        //             new GraphNode<BaseOperationViewModel, FilterLinkViewModel>(brushLinkModel.From);
+        //         let brush = FilterModel.GetFilterModelsRecursive(gnode, new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), brushFilterModels, false);
+        //         brushes.push(brush);
+        //     }
+        // });
+        // return brushes;
+    }
+
+
+    @computed.struct
+    public get SelectionString() {
+        return "";
+        // let filterModels = new Array<FilterModel>();
+        // let rdg = MainManager.Instance.MainViewModel.FilterReverseDependencyGraph;
+        // let graphNode: GraphNode<BaseOperationViewModel, FilterLinkViewModel>;
+        // if (rdg.has(this.TypedViewModel)) {
+        //     graphNode = MainManager.Instance.MainViewModel.FilterReverseDependencyGraph.get(this.TypedViewModel);
+        // }
+        // else {
+        //     graphNode = new GraphNode<BaseOperationViewModel, FilterLinkViewModel>(this.TypedViewModel);
+        // }
+        // return FilterModel.GetFilterModelsRecursive(graphNode, new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, false);
+    }
+
+    GetAggregateParameters(histoX: AttributeTransformationModel, histoY: AttributeTransformationModel, histoValue: AttributeTransformationModel) {
+        let allAttributes = new Array<AttributeTransformationModel>(histoX, histoY, histoValue);
+        allAttributes = ArrayUtil.Distinct(allAttributes.filter(a => a.AggregateFunction !== AggregateFunction.None));
+
+        let numericDataTypes = [DataType.Int, DataType.Double, DataType.Float];
+        let perBinAggregateParameters: AggregateParameters[] = ModelHelpers.GetAggregateParametersWithMargins(allAttributes);
+        let globalAggregateParameters: AggregateParameters[] = [];
+        [histoX, histoY]
+            .filter(a => a.AggregateFunction === AggregateFunction.None && ArrayUtil.Contains(numericDataTypes, a.AttributeModel.DataType))
+            .forEach(a => {
+                let avg = new AverageAggregateParameters();
+                avg.attributeParameters = ModelHelpers.GetAttributeParameters(a.AttributeModel);
+                globalAggregateParameters.push(avg);
+            });
+        return [perBinAggregateParameters, globalAggregateParameters];
+    }
+
+    @computed
+    get createOperationParamsCache() {
+        return this.CreateOperationParameters();
+    }
+
+    public QRange: QuantitativeBinRange | undefined;
+
+    public CreateOperationParameters(): HistogramOperationParameters | undefined {
+        if (this.X && this.Y && this.V) {
+            let [perBinAggregateParameters, globalAggregateParameters] = this.GetAggregateParameters(this.X, this.Y, this.V);
+            return new HistogramOperationParameters({
+                enableBrushComputation: true,
+                adapterName: Main.Instance.ActiveSchema!.displayName,
+                filter: this.FilterString,
+                brushes: this.BrushString,
+                binningParameters: [ModelHelpers.GetBinningParameters(this.X, SETTINGS_X_BINS, this.QRange ? this.QRange.minValue : undefined, this.QRange ? this.QRange.maxValue : undefined),
+                ModelHelpers.GetBinningParameters(this.Y, SETTINGS_Y_BINS)],
+                sampleStreamBlockSize: SETTINGS_SAMPLE_SIZE,
+                perBinAggregateParameters: perBinAggregateParameters,
+                globalAggregateParameters: globalAggregateParameters,
+                sortPerBinAggregateParameter: undefined,
+                attributeCalculatedParameters: CalculatedAttributeManager
+                    .AllCalculatedAttributes.map(a => ModelHelpers.GetAttributeParametersFromAttributeModel(a)),
+                degreeOfParallism: 1, // Settings.Instance.DegreeOfParallelism,
+                isCachable: false
+            });
+        }
+    }
+
+
+    @action
+    public async Update(): Promise<void> {
+        // this.TypedViewModel.BrushColors = this.TypedViewModel.BrusherModels.map(e => e.Color);
+        return super.Update();
+    }
+}
+
+
diff --git a/src/client/northstar/utils/ArrayUtil.ts b/src/client/northstar/utils/ArrayUtil.ts
new file mode 100644
index 000000000..f35c98317
--- /dev/null
+++ b/src/client/northstar/utils/ArrayUtil.ts
@@ -0,0 +1,90 @@
+import { Exception } from "../model/idea/idea";
+
+export class ArrayUtil {
+
+    public static Contains(arr1: any[], arr2: any): boolean {
+        if (arr1.length === 0) {
+            return false;
+        }
+        let isComplex = typeof arr1[0] === "object";
+        for (let i = 0; i < arr1.length; i++) {
+            if (isComplex && "Equals" in arr1[i]) {
+                if (arr1[i].Equals(arr2)) {
+                    return true;
+                }
+            }
+            else {
+                if (arr1[i] === arr2) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+
+    public static RemoveMany(arr: any[], elements: Object[]) {
+        elements.forEach(e => ArrayUtil.Remove(arr, e));
+    }
+
+    public static AddMany(arr: any[], others: Object[]) {
+        arr.push(...others);
+    }
+
+    public static Clear(arr: any[]) {
+        arr.splice(0, arr.length);
+    }
+
+
+    public static Remove(arr: any[], other: Object) {
+        const index = ArrayUtil.IndexOfWithEqual(arr, other);
+        if (index === -1) {
+            return;
+        }
+        arr.splice(index, 1);
+    }
+
+
+    public static First<T>(arr: T[], predicate: (x: any) => boolean): T {
+        let filtered = arr.filter(predicate);
+        if (filtered.length > 0) {
+            return filtered[0];
+        }
+        throw new Exception()
+    }
+
+    public static FirstOrDefault<T>(arr: T[], predicate: (x: any) => boolean): T | undefined {
+        let filtered = arr.filter(predicate);
+        if (filtered.length > 0) {
+            return filtered[0];
+        }
+        return undefined;
+    }
+
+    public static Distinct(arr: any[]): any[] {
+        let ret = [];
+        for (let i = 0; i < arr.length; i++) {
+            if (!ArrayUtil.Contains(ret, arr[i])) {
+                ret.push(arr[i]);
+            }
+        }
+        return ret;
+    }
+
+    public static IndexOfWithEqual(arr: any[], other: any): number {
+        for (let i = 0; i < arr.length; i++) {
+            let isComplex = typeof arr[0] === "object";
+            if (isComplex && "Equals" in arr[i]) {
+                if (arr[i].Equals(other)) {
+                    return i;
+                }
+            }
+            else {
+                if (arr[i] === other) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts
new file mode 100644
index 000000000..71bcadf89
--- /dev/null
+++ b/src/client/northstar/utils/Extensions.ts
@@ -0,0 +1,20 @@
+interface String {
+    ReplaceAll(toReplace: string, replacement: string): string;
+}
+
+String.prototype.ReplaceAll = function (toReplace: string, replacement: string): string {
+    var target = this;
+    return target.split(toReplace).join(replacement);
+}
+
+interface Math {
+    log10(val: number): number;
+}
+
+Math.log10 = function (val: number): number {
+    return Math.log(val) / Math.LN10;
+}
+
+declare interface ObjectConstructor {
+    assign(...objects: Object[]): Object;
+}
diff --git a/src/client/northstar/utils/GeometryUtil.ts b/src/client/northstar/utils/GeometryUtil.ts
new file mode 100644
index 000000000..d5f3ba631
--- /dev/null
+++ b/src/client/northstar/utils/GeometryUtil.ts
@@ -0,0 +1,129 @@
+import { MathUtil, PIXIRectangle, PIXIPoint } from "./MathUtil";
+
+
+export class GeometryUtil {
+
+    public static ComputeBoundingBox(points: { x: number, y: number }[], scale = 1, padding: number = 0): PIXIRectangle {
+        let minX: number = Number.MAX_VALUE;
+        let minY: number = Number.MAX_VALUE;
+        let maxX: number = Number.MIN_VALUE;
+        let maxY: number = Number.MIN_VALUE;
+        for (var i = 0; i < points.length; i++) {
+            if (points[i].x < minX)
+                minX = points[i].x;
+            if (points[i].y < minY)
+                minY = points[i].y;
+            if (points[i].x > maxX)
+                maxX = points[i].x;
+            if (points[i].y > maxY)
+                maxY = points[i].y;
+        }
+        return new PIXIRectangle(minX * scale - padding, minY * scale - padding, (maxX - minX) * scale + padding * 2, (maxY - minY) * scale + padding * 2);
+    }
+
+    public static RectangleOverlap(rect1: PIXIRectangle, rect2: PIXIRectangle) {
+        let x_overlap = Math.max(0, Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left));
+        let y_overlap = Math.max(0, Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top));
+        return x_overlap * y_overlap;
+    }
+
+    public static RotatePoints(center: { x: number, y: number }, points: { x: number, y: number }[], angle: number): PIXIPoint[] {
+        const rotate = (cx: number, cy: number, x: number, y: number, angle: number) => {
+            const radians = angle,
+                cos = Math.cos(radians),
+                sin = Math.sin(radians),
+                nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
+                ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
+            return new PIXIPoint(nx, ny);
+        }
+        return points.map(p => rotate(center.x, center.y, p.x, p.y, angle));
+    }
+
+    public static LineByLeastSquares(points: { x: number, y: number }[]): PIXIPoint[] {
+        let sum_x: number = 0;
+        let sum_y: number = 0;
+        let sum_xy: number = 0;
+        let sum_xx: number = 0;
+        let count: number = 0;
+
+        let x: number = 0;
+        let y: number = 0;
+
+
+        if (points.length === 0) {
+            return [];
+        }
+
+        for (let v = 0; v < points.length; v++) {
+            x = points[v].x;
+            y = points[v].y;
+            sum_x += x;
+            sum_y += y;
+            sum_xx += x * x;
+            sum_xy += x * y;
+            count++;
+        }
+
+        let m = (count * sum_xy - sum_x * sum_y) / (count * sum_xx - sum_x * sum_x);
+        let b = (sum_y / count) - (m * sum_x) / count;
+        let result: PIXIPoint[] = new Array<PIXIPoint>();
+
+        for (let v = 0; v < points.length; v++) {
+            x = points[v].x;
+            y = x * m + b;
+            result.push(new PIXIPoint(x, y));
+        }
+        return result;
+    }
+
+    // public static PointInsidePolygon(vs:Point[], x:number, y:number):boolean {
+    //     // ray-casting algorithm based on
+    //     // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
+
+    //     var inside = false;
+    //     for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
+    //         var xi = vs[i].x, yi = vs[i].y;
+    //         var xj = vs[j].x, yj = vs[j].y;
+
+    //         var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
+    //         if (intersect) 
+    //            inside = !inside;
+    //     }
+
+    //     return inside;
+    // };
+
+    public static IntersectLines(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): boolean {
+        let a1: number, a2: number, b1: number, b2: number, c1: number, c2: number;
+        let r1: number, r2: number, r3: number, r4: number;
+        let denom: number, offset: number, num: number;
+
+        a1 = y2 - y1;
+        b1 = x1 - x2;
+        c1 = (x2 * y1) - (x1 * y2);
+        r3 = ((a1 * x3) + (b1 * y3) + c1);
+        r4 = ((a1 * x4) + (b1 * y4) + c1);
+
+        if ((r3 !== 0) && (r4 !== 0) && (MathUtil.Sign(r3) === MathUtil.Sign(r4))) {
+            return false;
+        }
+
+        a2 = y4 - y3;
+        b2 = x3 - x4;
+        c2 = (x4 * y3) - (x3 * y4);
+
+        r1 = (a2 * x1) + (b2 * y1) + c2;
+        r2 = (a2 * x2) + (b2 * y2) + c2;
+
+        if ((r1 !== 0) && (r2 !== 0) && (MathUtil.Sign(r1) === MathUtil.Sign(r2))) {
+            return false;
+        }
+
+        denom = (a1 * b2) - (a2 * b1);
+
+        if (denom === 0) {
+            return false;
+        }
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/utils/IDisposable.ts b/src/client/northstar/utils/IDisposable.ts
new file mode 100644
index 000000000..5e9843326
--- /dev/null
+++ b/src/client/northstar/utils/IDisposable.ts
@@ -0,0 +1,3 @@
+export interface IDisposable {
+    Dispose(): void;
+}
\ No newline at end of file
diff --git a/src/client/northstar/utils/IEquatable.ts b/src/client/northstar/utils/IEquatable.ts
new file mode 100644
index 000000000..2f81c2478
--- /dev/null
+++ b/src/client/northstar/utils/IEquatable.ts
@@ -0,0 +1,3 @@
+export interface IEquatable {
+    Equals(other: Object): boolean;
+}
\ No newline at end of file
diff --git a/src/client/northstar/utils/KeyCodes.ts b/src/client/northstar/utils/KeyCodes.ts
new file mode 100644
index 000000000..044569ffe
--- /dev/null
+++ b/src/client/northstar/utils/KeyCodes.ts
@@ -0,0 +1,137 @@
+/**
+ * Class contains the keycodes for keys on your keyboard.
+ *
+ * Useful for auto completion:
+ *
+ * ```
+ * switch (event.key)
+ * {
+ *      case KeyCode.UP:
+ *      {
+ *      	// Up key pressed
+ *       	break;
+ *      }
+ *      case KeyCode.DOWN:
+ *      {
+ *      	// Down key pressed
+ *       	break;
+ *      }
+ *      case KeyCode.LEFT:
+ *      {
+ *      	// Left key pressed
+ *       	break;
+ *      }
+ *      case KeyCode.RIGHT:
+ *      {
+ *      	// Right key pressed
+ *       	break;
+ *      }
+ *      default:
+ *      {
+ *      	// ignore
+ *      	break;
+ *      }
+ * }
+ * ```
+ */
+export class KeyCodes
+{
+	public static TAB:number = 9;
+	public static CAPS_LOCK:number = 20;
+	public static SHIFT:number = 16;
+	public static CONTROL:number = 17;
+	public static SPACE:number = 32;
+	public static DOWN:number = 40;
+	public static UP:number = 38;
+	public static LEFT:number = 37;
+	public static RIGHT:number = 39;
+	public static ESCAPE:number = 27;
+	public static F1:number = 112;
+	public static F2:number = 113;
+	public static F3:number = 114;
+	public static F4:number = 115;
+	public static F5:number = 116;
+	public static F6:number = 117;
+	public static F7:number = 118;
+	public static F8:number = 119;
+	public static F9:number = 120;
+	public static F10:number = 121;
+	public static F11:number = 122;
+	public static F12:number = 123;
+	public static INSERT:number = 45;
+	public static HOME:number = 36;
+	public static PAGE_UP:number = 33;
+	public static PAGE_DOWN:number = 34;
+	public static DELETE:number = 46;
+	public static END:number = 35;
+	public static ENTER:number = 13;
+	public static BACKSPACE:number = 8;
+	public static NUMPAD_0:number = 96;
+	public static NUMPAD_1:number = 97;
+	public static NUMPAD_2:number = 98;
+	public static NUMPAD_3:number = 99;
+	public static NUMPAD_4:number = 100;
+	public static NUMPAD_5:number = 101;
+	public static NUMPAD_6:number = 102;
+	public static NUMPAD_7:number = 103;
+	public static NUMPAD_8:number = 104;
+	public static NUMPAD_9:number = 105;
+	public static NUMPAD_DIVIDE:number = 111;
+	public static NUMPAD_ADD:number = 107;
+	public static NUMPAD_ENTER:number = 13;
+	public static NUMPAD_DECIMAL:number = 110;
+	public static NUMPAD_SUBTRACT:number = 109;
+	public static NUMPAD_MULTIPLY:number = 106;
+	public static SEMICOLON:number = 186;
+	public static EQUAL:number = 187;
+	public static COMMA:number = 188;
+	public static MINUS:number = 189;
+	public static PERIOD:number = 190;
+	public static SLASH:number = 191;
+	public static BACKQUOTE:number = 192;
+	public static LEFTBRACKET:number = 219;
+	public static BACKSLASH:number = 220;
+	public static RIGHTBRACKET:number = 221;
+	public static QUOTE:number = 222;
+	public static ALT:number = 18;
+	public static COMMAND:number = 15;
+	public static NUMPAD:number = 21;
+	public static A:number = 65;
+	public static B:number = 66;
+	public static C:number = 67;
+	public static D:number = 68;
+	public static E:number = 69;
+	public static F:number = 70;
+	public static G:number = 71;
+	public static H:number = 72;
+	public static I:number = 73;
+	public static J:number = 74;
+	public static K:number = 75;
+	public static L:number = 76;
+	public static M:number = 77;
+	public static N:number = 78;
+	public static O:number = 79;
+	public static P:number = 80;
+	public static Q:number = 81;
+	public static R:number = 82;
+	public static S:number = 83;
+	public static T:number = 84;
+	public static U:number = 85;
+	public static V:number = 86;
+	public static W:number = 87;
+	public static X:number = 88;
+	public static Y:number = 89;
+	public static Z:number = 90;
+	public static NUM_0:number = 48;
+	public static NUM_1:number = 49;
+	public static NUM_2:number = 50;
+	public static NUM_3:number = 51;
+	public static NUM_4:number = 52;
+	public static NUM_5:number = 53;
+	public static NUM_6:number = 54;
+	public static NUM_7:number = 55;
+	public static NUM_8:number = 56;
+	public static NUM_9:number = 57;
+	public static SUBSTRACT:number = 189;
+	public static ADD:number = 187;
+}
\ No newline at end of file
diff --git a/src/client/northstar/utils/MathUtil.ts b/src/client/northstar/utils/MathUtil.ts
new file mode 100644
index 000000000..3ed8628ee
--- /dev/null
+++ b/src/client/northstar/utils/MathUtil.ts
@@ -0,0 +1,236 @@
+
+
+export class PIXIPoint {
+    public x: number;
+    public y: number;
+    constructor(x: number, y: number) {
+        this.x = x;
+        this.y = y;
+    }
+}
+export class PIXIRectangle {
+    public x: number;
+    public y: number;
+    public width: number;
+    public height: number
+    public get left() { return this.x }
+    public get right() { return this.x + this.width; }
+    public get top() { return this.y }
+    public get bottom() { return this.top + this.height }
+    constructor(x: number, y: number, width: number, height: number) {
+        this.x = x;
+        this.y = y;
+        this.width = width;
+        this.height = height;
+    }
+}
+
+export class MathUtil {
+
+    public static EPSILON: number = 0.001;
+
+    public static Sign(value: number): number {
+        return value >= 0 ? 1 : -1;
+    }
+
+    public static AddPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
+        if (inline) {
+            p1.x += p2.x;
+            p1.y += p2.y;
+            return p1;
+        }
+        else {
+            return new PIXIPoint(p1.x + p2.x, p1.y + p2.y);
+        }
+    }
+
+    public static Perp(p1: PIXIPoint): PIXIPoint {
+        return new PIXIPoint(-p1.y, p1.x);
+    }
+
+    public static DividePoint(p1: PIXIPoint, by: number, inline: boolean = false): PIXIPoint {
+        if (inline) {
+            p1.x /= by;
+            p1.y /= by;
+            return p1;
+        }
+        else {
+            return new PIXIPoint(p1.x / by, p1.y / by);
+        }
+    }
+
+    public static MultiplyConstant(p1: PIXIPoint, by: number, inline: boolean = false) {
+        if (inline) {
+            p1.x *= by;
+            p1.y *= by;
+            return p1;
+        }
+        else {
+            return new PIXIPoint(p1.x * by, p1.y * by);
+        }
+    }
+
+    public static SubtractPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
+        if (inline) {
+            p1.x -= p2.x;
+            p1.y -= p2.y;
+            return p1;
+        }
+        else {
+            return new PIXIPoint(p1.x - p2.x, p1.y - p2.y);
+        }
+    }
+
+    public static Area(rect: PIXIRectangle): number {
+        return rect.width * rect.height;
+    }
+
+    public static DistToLineSegment(v: PIXIPoint, w: PIXIPoint, p: PIXIPoint) {
+        // Return minimum distance between line segment vw and point p
+        var l2 = MathUtil.DistSquared(v, w);  // i.e. |w-v|^2 -  avoid a sqrt
+        if (l2 == 0.0) return MathUtil.Dist(p, v);   // v == w case
+        // Consider the line extending the segment, parameterized as v + t (w - v).
+        // We find projection of point p onto the line. 
+        // It falls where t = [(p-v) . (w-v)] / |w-v|^2
+        // We clamp t from [0,1] to handle points outside the segment vw.
+        var dot = MathUtil.Dot(
+            MathUtil.SubtractPoint(p, v),
+            MathUtil.SubtractPoint(w, v)) / l2;
+        var t = Math.max(0, Math.min(1, dot));
+        // Projection falls on the segment
+        var projection = MathUtil.AddPoint(v,
+            MathUtil.MultiplyConstant(
+                MathUtil.SubtractPoint(w, v), t));
+        return MathUtil.Dist(p, projection);
+    }
+
+    public static LineSegmentIntersection(ps1: PIXIPoint, pe1: PIXIPoint, ps2: PIXIPoint, pe2: PIXIPoint): PIXIPoint | undefined {
+        var a1 = pe1.y - ps1.y;
+        var b1 = ps1.x - pe1.x;
+
+        var a2 = pe2.y - ps2.y;
+        var b2 = ps2.x - pe2.x;
+
+        var delta = a1 * b2 - a2 * b1;
+        if (delta == 0) {
+            return undefined;
+        }
+        var c2 = a2 * ps2.x + b2 * ps2.y;
+        var c1 = a1 * ps1.x + b1 * ps1.y;
+        var invdelta = 1 / delta;
+        return new PIXIPoint((b2 * c1 - b1 * c2) * invdelta, (a1 * c2 - a2 * c1) * invdelta);
+    }
+
+    public static PointInPIXIRectangle(p: PIXIPoint, rect: PIXIRectangle): boolean {
+        if (p.x < rect.left - this.EPSILON)
+            return false;
+        if (p.x > rect.right + this.EPSILON)
+            return false;
+        if (p.y < rect.top - this.EPSILON)
+            return false;
+        if (p.y > rect.bottom + this.EPSILON)
+            return false;
+
+        return true;
+    }
+
+    public static LinePIXIRectangleIntersection(lineFrom: PIXIPoint, lineTo: PIXIPoint, rect: PIXIRectangle): Array<PIXIPoint> {
+        var r1 = new PIXIPoint(rect.left, rect.top);
+        var r2 = new PIXIPoint(rect.right, rect.top);
+        var r3 = new PIXIPoint(rect.right, rect.bottom);
+        var r4 = new PIXIPoint(rect.left, rect.bottom);
+        var ret = new Array<PIXIPoint>();
+        var dist = this.Dist(lineFrom, lineTo)
+        var inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2);
+        if (inter != null && this.PointInPIXIRectangle(inter, rect) &&
+            this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist)
+            ret.push(inter);
+        inter = this.LineSegmentIntersection(lineFrom, lineTo, r2, r3);
+        if (inter != null && this.PointInPIXIRectangle(inter, rect) &&
+            this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist)
+            ret.push(inter);
+        inter = this.LineSegmentIntersection(lineFrom, lineTo, r3, r4);
+        if (inter != null && this.PointInPIXIRectangle(inter, rect) &&
+            this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist)
+            ret.push(inter);
+        inter = this.LineSegmentIntersection(lineFrom, lineTo, r4, r1);
+        if (inter != null && this.PointInPIXIRectangle(inter, rect) &&
+            this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist)
+            ret.push(inter);
+        return ret;
+    }
+
+    public static Intersection(rect1: PIXIRectangle, rect2: PIXIRectangle): PIXIRectangle {
+        const left = Math.max(rect1.x, rect2.x);
+        const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
+        const top = Math.max(rect1.y, rect2.y);
+        const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
+        return new PIXIRectangle(left, top, right - left, bottom - top);
+    }
+
+    public static Dist(p1: PIXIPoint, p2: PIXIPoint): number {
+        return Math.sqrt(MathUtil.DistSquared(p1, p2));
+    }
+
+    public static Dot(p1: PIXIPoint, p2: PIXIPoint): number {
+        return p1.x * p2.x + p1.y * p2.y
+    }
+
+    public static Normalize(p1: PIXIPoint) {
+        var d = this.Length(p1);
+        return new PIXIPoint(p1.x / d, p1.y / d);
+    }
+
+    public static Length(p1: PIXIPoint): number {
+        return Math.sqrt(p1.x * p1.x + p1.y * p1.y);
+    }
+
+    public static DistSquared(p1: PIXIPoint, p2: PIXIPoint): number {
+        const a = p1.x - p2.x;
+        const b = p1.y - p2.y;
+        return (a * a + b * b)
+    }
+
+    public static RectIntersectsRect(r1: PIXIRectangle, r2: PIXIRectangle): boolean {
+        return !(r2.x > r1.x + r1.width ||
+            r2.x + r2.width < r1.x ||
+            r2.y > r1.y + r1.height ||
+            r2.y + r2.height < r1.y);
+    }
+
+    public static ArgMin(temp: number[]): number {
+        let index = 0;
+        let value = temp[0];
+        for (let i = 1; i < temp.length; i++) {
+            if (temp[i] < value) {
+                value = temp[i];
+                index = i;
+            }
+        }
+        return index;
+    }
+
+    public static ArgMax(temp: number[]): number {
+        let index = 0;
+        let value = temp[0];
+        for (let i = 1; i < temp.length; i++) {
+            if (temp[i] > value) {
+                value = temp[i];
+                index = i;
+            }
+        }
+        return index;
+    }
+
+    public static Combinations<T>(chars: T[]) {
+        let result = new Array<T>();
+        let f = (prefix: any, chars: any) => {
+            for (let i = 0; i < chars.length; i++) {
+                result.push(prefix.concat(chars[i]));
+                f(prefix.concat(chars[i]), chars.slice(i + 1));
+            }
+        };
+        f([], chars);
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/utils/PartialClass.ts b/src/client/northstar/utils/PartialClass.ts
new file mode 100644
index 000000000..2f20de96f
--- /dev/null
+++ b/src/client/northstar/utils/PartialClass.ts
@@ -0,0 +1,7 @@
+
+export class PartialClass<T> {
+
+    constructor(data?: Partial<T>) {
+        Object.assign(this, data);
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/utils/Utils.ts b/src/client/northstar/utils/Utils.ts
new file mode 100644
index 000000000..b35dce820
--- /dev/null
+++ b/src/client/northstar/utils/Utils.ts
@@ -0,0 +1,75 @@
+import { IBaseBrushable } from '../core/brusher/IBaseBrushable'
+import { IBaseFilterConsumer } from '../core/filter/IBaseFilterConsumer'
+import { IBaseFilterProvider } from '../core/filter/IBaseFilterProvider'
+import { AggregateFunction } from '../model/idea/idea'
+
+export class Utils {
+
+    public static EqualityHelper(a: Object, b: Object): boolean {
+        if (a === b) return true;
+        if (a === undefined && b !== undefined) return false;
+        if (a === null && b !== null) return false;
+        if (b === undefined && a !== undefined) return false;
+        if (b === null && a !== null) return false;
+        if ((<any>a).constructor.name !== (<any>b).constructor.name) return false;
+        return true;
+    }
+
+    public static LowercaseFirstLetter(str: string) {
+        return str.charAt(0).toUpperCase() + str.slice(1);
+    }
+
+    //
+    // this Type Guard tests if dropTarget is an IDropTarget.  If it is, it coerces the compiler
+    // to treat the dropTarget parameter as an IDropTarget *ouside* this function scope (ie, in
+    // the scope of where this function is called from).
+    //
+
+    public static isBaseBrushable<T>(obj: Object): obj is IBaseBrushable<T> {
+        let typed = <IBaseBrushable<T>>obj;
+        return typed != null && typed.BrusherModels !== undefined;
+    }
+
+    public static isBaseFilterProvider(obj: Object): obj is IBaseFilterProvider {
+        let typed = <IBaseFilterProvider>obj;
+        return typed != null && typed.FilterModels !== undefined;
+    }
+
+    public static isBaseFilterConsumer(obj: Object): obj is IBaseFilterConsumer {
+        let typed = <IBaseFilterConsumer>obj;
+        return typed != null && typed.FilterOperand !== undefined;
+    }
+
+    public static EncodeQueryData(data: any): string {
+        const ret = [];
+        for (let d in data) {
+            ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
+        }
+        return ret.join("&");
+    }
+
+    public static ToVegaAggregationString(agg: AggregateFunction): string {
+        if (agg === AggregateFunction.Avg) {
+            return "average";
+        }
+        else if (agg === AggregateFunction.Count) {
+            return "count";
+        }
+        else {
+            return "";
+        }
+    }
+
+    public static GetQueryVariable(variable: string) {
+        let query = window.location.search.substring(1);
+        let vars = query.split("&");
+        for (let i = 0; i < vars.length; i++) {
+            let pair = vars[i].split("=");
+            if (decodeURIComponent(pair[0]) == variable) {
+                return decodeURIComponent(pair[1]);
+            }
+        }
+        return undefined;
+    }
+}
+
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index ac51a7d87..b27f63e52 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,4 +1,4 @@
-import { action, configure, observable, runInAction, trace, computed } from 'mobx';
+import { action, configure, observable, runInAction, trace, computed, reaction } from 'mobx';
 import "normalize.css";
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
@@ -22,7 +22,6 @@ import "./Main.scss";
 import { observer } from 'mobx-react';
 import { InkingControl } from './InkingControl';
 import { RouteStore } from '../../server/RouteStore';
-import { json } from 'body-parser';
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faFont } from '@fortawesome/free-solid-svg-icons';
@@ -42,9 +41,10 @@ import { ServerUtils } from '../../server/ServerUtil';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 import { Field, Opt } from '../../fields/Field';
 import { ListField } from '../../fields/ListField';
-import { map } from 'bluebird';
 import { Gateway, Settings } from '../northstar/manager/Gateway';
-import { Catalog } from '../northstar/model/idea/idea';
+import { Catalog, Schema, Attribute, AttributeGroup } from '../northstar/model/idea/idea';
+import { ArrayUtil } from '../northstar/utils/ArrayUtil';
+import '../northstar/model/ModelExtensions'
 
 @observer
 export class Main extends React.Component {
@@ -88,16 +88,45 @@ export class Main extends React.Component {
 
         this.initEventListeners();
         Documents.initProtos(() => this.initAuthenticationRouters());
+
+        reaction(() => [this.mainContainer, this.ActiveSchema],
+            () => {
+                if (this.mainContainer && this.ActiveSchema) {
+                    if (!this.mainContainer!.GetTAsync(KeyStore.ActiveDB, ListField, field => this.NorthstarCatalog = field!.Data as Document[])) {
+                        this.NorthstarCatalog = this.GetAllAttributes().map(a => Documents.HistogramDocument({ width: 200, height: 200, title: a.displayName! }));
+                        this.mainContainer!.SetData(KeyStore.ActiveDB, this.NorthstarCatalog, ListField);
+                    }
+                }
+            })
     }
 
+    NorthstarCatalog: Document[] = [];
+    @observable ActiveSchema: Schema | undefined;
     @action SetNorthstarCatalog(ctlog: Catalog) {
         this._northstarCatalog = ctlog;
-        if (this._northstarCatalog) {
+        if (this._northstarCatalog && this._northstarCatalog.schemas) {
             console.log("CATALOG " + this._northstarCatalog.schemas);
+            this.ActiveSchema = ArrayUtil.FirstOrDefault<Schema>(this._northstarCatalog.schemas!, (s: Schema) => s.displayName === "mimic");
         }
     }
+    public GetAllAttributes() {
+        if (!this.ActiveSchema || !this.ActiveSchema.rootAttributeGroup) {
+            return [];
+        }
+        const recurs = (attrs: Attribute[], g: AttributeGroup) => {
+            if (g.attributes) {
+                attrs.push.apply(attrs, g.attributes);
+                if (g.attributeGroups) {
+                    g.attributeGroups.forEach(ng => recurs(attrs, ng));
+                }
+            }
+        };
+        const allAttributes: Attribute[] = new Array<Attribute>();
+        recurs(allAttributes, this.ActiveSchema.rootAttributeGroup);
+        return allAttributes;
+    }
     async initializeNorthstar(): Promise<void> {
-        let envPath = "assets/env.json";
+        let envPath = "/assets/env.json";
         const response = await fetch(envPath, {
             redirect: "follow",
             method: "GET",
@@ -251,7 +280,7 @@ export class Main extends React.Component {
 
         let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" }))
         let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
-        let addSchemaNode = action(() => Documents.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" }));
+        let addSchemaNode = action(() => Documents.SchemaDocument(this.NorthstarCatalog, { width: 200, height: 200, title: "a schema collection" }));
         let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, height: 200, title: "video node" }));
         let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" }));
         let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" }));
@@ -269,7 +298,9 @@ export class Main extends React.Component {
             [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
         ]
 
-        let addClick = (creator: () => Document) => action(() => this.mainfreeform!.GetList<Document>(KeyStore.Data, []).push(creator()));
+        let addClick = (creator: () => Document) => action(() => {
+            this.mainfreeform!.GetList<Document>(KeyStore.Data, []).push(creator())
+        });
 
         return < div id="add-nodes-menu" >
             <input type="checkbox" id="add-menu-toggle" />
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index ce72ab64b..2f0459f88 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -19,6 +19,7 @@ import { KeyValueBox } from "./KeyValueBox";
 import { PDFBox } from "./PDFBox";
 import { VideoBox } from "./VideoBox";
 import { WebBox } from "./WebBox";
+import { HistogramBox } from "./HistogramBox";
 import React = require("react");
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
@@ -47,8 +48,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
 
     render() {
         return <JsxParser
-            components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox }}
-            bindings={this.CreateBindings()}
+            components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} bindings={this.CreateBindings()}
             jsx={this.layout}
             showWarnings={true}
             onError={(test: any) => { console.log(test) }}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index b6d50bffb..f6343c631 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -7,7 +7,6 @@ import { TextField } from "../../../fields/TextField";
 import { NumberField } from "../../../fields/NumberField";
 import { RichTextField } from "../../../fields/RichTextField";
 import { ImageField } from "../../../fields/ImageField";
-import { WebField } from "../../../fields/WebField";
 import { VideoField } from "../../../fields/VideoField"
 import { Key } from "../../../fields/Key";
 import { FormattedTextBox } from "./FormattedTextBox";
@@ -64,9 +63,11 @@ export class FieldView extends React.Component<FieldViewProps> {
         }
         else if (field instanceof AudioField) {
             return <AudioBox {...this.props} />
-        } else if (field instanceof Document) {
+        }
+        else if (field instanceof Document) {
             return <div>{field.Title}</div>
-        } else if (field instanceof ListField) {
+        }
+        else if (field instanceof ListField) {
             return (<div>
                 {(field as ListField<Field>).Data.map(f => {
                     return f instanceof Document ? f.Title : f.GetValue().toString();
diff --git a/src/client/views/nodes/HistogramBox.scss b/src/client/views/nodes/HistogramBox.scss
new file mode 100644
index 000000000..04bf1d732
--- /dev/null
+++ b/src/client/views/nodes/HistogramBox.scss
@@ -0,0 +1,8 @@
+.histogrambox-container {
+    padding: 0vw;
+    position: relative;
+    text-align: center;
+    width: 100%;
+    height: 100%;
+  }
+  
\ No newline at end of file
diff --git a/src/client/views/nodes/HistogramBox.tsx b/src/client/views/nodes/HistogramBox.tsx
new file mode 100644
index 000000000..0fcc25e66
--- /dev/null
+++ b/src/client/views/nodes/HistogramBox.tsx
@@ -0,0 +1,67 @@
+import React = require("react")
+import { observer } from "mobx-react";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./VideoBox.scss";
+import { observable, reaction } from "mobx";
+import { HistogramOperation } from "../../northstar/operations/HistogramOperation";
+import { Main } from "../Main";
+import { ColumnAttributeModel } from "../../northstar/core/attribute/AttributeModel";
+import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel";
+import { AggregateFunction, HistogramResult, DoubleValueAggregateResult } from "../../northstar/model/idea/idea";
+import { ModelHelpers } from "../../northstar/model/ModelHelpers";
+
+@observer
+export class HistogramBox extends React.Component<FieldViewProps> {
+
+    public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(HistogramBox, fieldStr) }
+
+    constructor(props: FieldViewProps) {
+        super(props);
+    }
+
+    @observable _histoResult?: HistogramResult;
+    _histoOp?: HistogramOperation;
+
+    componentDidMount() {
+        Main.Instance.GetAllAttributes().map(a => {
+            if (a.displayName == this.props.doc.Title) {
+                var atmod = new ColumnAttributeModel(a);
+                this._histoOp = new HistogramOperation(new AttributeTransformationModel(atmod, AggregateFunction.None),
+                    new AttributeTransformationModel(atmod, AggregateFunction.Count),
+                    new AttributeTransformationModel(atmod, AggregateFunction.Count));
+                reaction(() => [this._histoOp && this._histoOp.Result],
+                    () => this._histoResult = this._histoOp ? this._histoOp.Result as HistogramResult : undefined
+                );
+                this._histoOp.Update();
+            }
+        })
+    }
+
+    twoString() {
+        let str = "";
+        if (this._histoResult && !this._histoResult.isEmpty) {
+            for (let key in this._histoResult.bins) {
+                if (this._histoResult.bins.hasOwnProperty(key)) {
+                    let bin = this._histoResult.bins[key];
+                    str += JSON.stringify(bin.binIndex!.toJSON()) + " = ";
+                    let valueAggregateKey = ModelHelpers.CreateAggregateKey(this._histoOp!.V, this._histoResult, ModelHelpers.AllBrushIndex(this._histoResult));
+                    let value = ModelHelpers.GetAggregateResult(bin, valueAggregateKey) as DoubleValueAggregateResult;
+                    if (value && value.hasResult && value.result) {
+                        str += value.result;
+                    }
+                }
+            }
+        }
+        return str;
+    }
+
+    render() {
+        if (!this._histoResult)
+            return (null);
+        return (
+            <div className="histogrambox-container">
+                `HISTOGRAM RESULT : ${this.twoString()}`
+            </div>
+        )
+    }
+}
\ No newline at end of file
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
index 68883d6f1..319702cc4 100644
--- a/src/fields/KeyStore.ts
+++ b/src/fields/KeyStore.ts
@@ -28,6 +28,7 @@ export namespace KeyStore {
     export const SchemaSplitPercentage = new Key("SchemaSplitPercentage");
     export const Caption = new Key("Caption");
     export const ActiveFrame = new Key("ActiveFrame");
+    export const ActiveDB = new Key("ActiveDB");
     export const DocumentText = new Key("DocumentText");
     export const LinkedToDocs = new Key("LinkedToDocs");
     export const LinkedFromDocs = new Key("LinkedFromDocs");
-- 
cgit v1.2.3-70-g09d2


From 356991c6100a44ef45b4574b43c815383d9be751 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sat, 23 Mar 2019 00:22:52 -0400
Subject: Various fixes for the demo

---
 src/client/documents/Documents.ts                  | 17 ++++++---
 src/client/util/Scripting.ts                       |  8 +++--
 src/client/util/type_decls.d                       |  3 ++
 .../views/collections/CollectionFreeFormView.tsx   |  5 +++
 .../views/collections/CollectionSchemaView.tsx     | 42 ++++++++++++++++++----
 .../views/collections/CollectionViewBase.tsx       | 39 ++++++++++++++------
 src/client/views/nodes/DocumentView.tsx            |  7 ++++
 src/client/views/nodes/FieldView.tsx               | 18 +++++++++-
 src/client/views/nodes/FormattedTextBox.tsx        |  2 ++
 9 files changed, 117 insertions(+), 24 deletions(-)

(limited to 'src/client/views/nodes/FieldView.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 96a7332aa..a2444db06 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,6 +1,6 @@
 import { AudioField } from "../../fields/AudioField";
 import { Document } from "../../fields/Document";
-import { Field } from "../../fields/Field";
+import { Field, FieldWaiting } from "../../fields/Field";
 import { HtmlField } from "../../fields/HtmlField";
 import { ImageField } from "../../fields/ImageField";
 import { InkField, StrokeData } from "../../fields/InkField";
@@ -23,6 +23,7 @@ import { PDFBox } from "../views/nodes/PDFBox";
 import { VideoBox } from "../views/nodes/VideoBox";
 import { WebBox } from "../views/nodes/WebBox";
 import { HistogramBox } from "../views/nodes/HistogramBox";
+import { FieldView } from "../views/nodes/FieldView";
 
 export interface DocumentOptions {
     x?: number;
@@ -112,7 +113,7 @@ export namespace Documents {
     function GetImagePrototype(): Document {
         if (!imageProto) {
             imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("AnnotationsKey"),
-                { x: 0, y: 0, nativeWidth: 300, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] });
+                { x: 0, y: 0, nativeWidth: 300, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
             imageProto.SetText(KeyStore.BackgroundLayout, ImageBox.LayoutString());
         }
         return imageProto;
@@ -155,7 +156,7 @@ export namespace Documents {
     function GetVideoPrototype(): Document {
         if (!videoProto) {
             videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("AnnotationsKey"),
-                { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] });
+                { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
             videoProto.SetNumber(KeyStore.CurPage, 0);
             videoProto.SetText(KeyStore.BackgroundLayout, VideoBox.LayoutString());
         }
@@ -218,6 +219,14 @@ export namespace Documents {
         return assignToDelegate(SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Docking }, [config, TextField], id), options)
     }
 
+    export function CaptionDocument(doc: Document) {
+        const captionDoc = doc.CreateAlias();
+        captionDoc.SetText(KeyStore.OverlayLayout, FixedCaption());
+        captionDoc.SetNumber(KeyStore.Width, doc.GetNumber(KeyStore.Width, 0));
+        captionDoc.SetNumber(KeyStore.Height, doc.GetNumber(KeyStore.Height, 0));
+        return captionDoc;
+    }
+
     // example of custom display string for an image that shows a caption.
     function EmbeddedCaption() {
         return `<div style="height:100%">
@@ -228,7 +237,7 @@ export namespace Documents {
             + FormattedTextBox.LayoutString("CaptionKey") +
             `</div> 
         </div>` };
-    function FixedCaption(fieldName: string = "Caption") {
+    export function FixedCaption(fieldName: string = "Caption") {
         return `<div style="position:absolute; height:30px; bottom:0; width:100%">
             <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">`
             + FormattedTextBox.LayoutString(fieldName + "Key") +
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 4cf98472f..4e97b9401 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -15,6 +15,8 @@ import { ListField } from "../../fields/ListField";
 
 // @ts-ignore
 import * as typescriptlib from '!!raw-loader!./type_decls.d'
+import { Documents } from "../documents/Documents";
+import { Key } from "../../fields/Key";
 
 
 export interface ExecutableScript {
@@ -28,9 +30,9 @@ function Compile(script: string | undefined, diagnostics: Opt<any[]>, scope: { [
 
     let func: () => Opt<Field>;
     if (compiled && script) {
-        let fieldTypes = [Document, NumberField, TextField, ImageField, RichTextField, ListField];
-        let paramNames = ["KeyStore", ...fieldTypes.map(fn => fn.name)];
-        let params: any[] = [KeyStore, ...fieldTypes]
+        let fieldTypes = [Document, NumberField, TextField, ImageField, RichTextField, ListField, Key];
+        let paramNames = ["KeyStore", "Documents", ...fieldTypes.map(fn => fn.name)];
+        let params: any[] = [KeyStore, Documents, ...fieldTypes]
         for (let prop in scope) {
             if (prop === "this") {
                 continue;
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index dcf79285d..4f69053b1 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -174,6 +174,7 @@ declare class ListField<T> extends BasicField<T[]>{
     Copy(): Field;
 }
 declare class Key extends Field {
+    constructor(name:string);
     Name: string;
     TrySetValue(value: any): boolean;
     GetValue(): any;
@@ -220,3 +221,5 @@ declare const KeyStore: {
 
 // @ts-ignore
 declare const console: any;
+
+declare const Documents: any;
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index da9f7b392..8f7b4cfe7 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -83,6 +83,10 @@ export class CollectionFreeFormView extends CollectionViewBase {
             const [x, y] = this.getTransform().transformPoint(screenX, screenY);
             de.data.droppedDocument.SetNumber(KeyStore.X, x);
             de.data.droppedDocument.SetNumber(KeyStore.Y, y);
+            if (!de.data.droppedDocument.GetNumber(KeyStore.Width, 0)) {
+                de.data.droppedDocument.SetNumber(KeyStore.Width, 300);
+                de.data.droppedDocument.SetNumber(KeyStore.Height, 300);
+            }
             this.bringToFront(de.data.droppedDocument);
         }
     }
@@ -259,6 +263,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
         const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
         if (lvalue && lvalue != FieldWaiting) {
             return lvalue.Data.map(doc => {
+                if (!doc) return null;
                 var page = doc.GetNumber(KeyStore.Page, 0);
                 return (page != curPage && page != 0) ? (null) :
                     (<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 8e0a38f05..7dd364449 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -1,6 +1,6 @@
 import React = require("react")
 import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCog } from '@fortawesome/free-solid-svg-icons';
+import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable, trace, untracked } from "mobx";
 import { observer } from "mobx-react";
@@ -8,7 +8,7 @@ import Measure from "react-measure";
 import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
 import "react-table/react-table.css";
 import { Document } from "../../../fields/Document";
-import { Field, Opt } from "../../../fields/Field";
+import { Field, Opt, FieldWaiting } from "../../../fields/Field";
 import { Key } from "../../../fields/Key";
 import { KeyStore } from "../../../fields/KeyStore";
 import { ListField } from "../../../fields/ListField";
@@ -24,6 +24,7 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView";
 import "./CollectionSchemaView.scss";
 import { CollectionView, COLLECTION_BORDER_WIDTH } from "./CollectionView";
 import { CollectionViewBase } from "./CollectionViewBase";
+import { TextField } from "../../../fields/TextField";
 
 
 // bcz: need to add drag and drop of rows and columns.  This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
@@ -104,11 +105,11 @@ export class CollectionSchemaView extends CollectionViewBase {
             return false;
         }
         return (
-            <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} style={{ height: "36px" }} key={props.doc.Id} ref={reference}>
+            <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} style={{ height: "56px" }} key={props.doc.Id} ref={reference}>
                 <EditableView
                     display={"inline"}
                     contents={contents}
-                    height={36}
+                    height={56}
                     GetValue={() => {
                         let field = props.doc.Get(props.fieldKey);
                         if (field && field instanceof Field) {
@@ -249,23 +250,48 @@ export class CollectionSchemaView extends CollectionViewBase {
         }
     }
 
+    @action
+    addColumn = () => {
+        this.columns.push(new Key(this.newKeyName));
+        this.newKeyName = "";
+    }
+
+    @observable
+    newKeyName: string = "";
+
+    @action
+    newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this.newKeyName = e.currentTarget.value;
+    }
+
     @observable _optionsActivated: number = 0;
     @action
     OptionsMenuDown = (e: React.PointerEvent) => {
         this._optionsActivated++;
     }
+
+    @observable previewScript: string = "this";
+    @action
+    onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this.previewScript = e.currentTarget.value;
+    }
+
     render() {
         library.add(faCog);
+        library.add(faPlus);
         const columns = this.columns;
         const children = this.props.Document.GetList<Document>(this.props.fieldKey, []);
         const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
         //all the keys/columns that will be displayed in the schema
         const allKeys = this.findAllDocumentKeys;
+        let doc: any = selected ? selected.Get(new Key(this.previewScript)) : undefined;
+
+        // let doc = CompileScript(this.previewScript, { this: selected }, true)();
         let content = this._selectedIndex == -1 || !selected ? (null) : (
             <Measure onResize={this.setScaling}>
                 {({ measureRef }) =>
                     <div className="collectionSchemaView-content" ref={measureRef}>
-                        <DocumentView Document={selected}
+                        {doc instanceof Document ? <DocumentView Document={doc}
                             AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument}
                             isTopMost={false}
                             SelectOnLoad={false}
@@ -275,7 +301,9 @@ export class CollectionSchemaView extends CollectionViewBase {
                             PanelHeight={this.getPanelHeight}
                             ContainingCollectionView={this.props.CollectionView}
                             focus={this.focusDocument}
-                        />
+                        /> : null}
+                        <input value={this.previewScript} onChange={this.onPreviewScriptChange}
+                            style={{ position: 'absolute', bottom: '0px' }} />
                     </div>
                 }
             </Measure>
@@ -297,6 +325,8 @@ export class CollectionSchemaView extends CollectionViewBase {
                             return (<KeyToggle checked={allKeys[item]} key={item} keyId={item} toggle={this.toggleKey} />)
                         })}
                     </ul>
+                    <input value={this.newKeyName} onChange={this.newKeyChange} />
+                    <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button>
                 </div>
             </div>
             }>
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index f33007196..7d903899d 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -17,6 +17,7 @@ import { NumberField } from "../../../fields/NumberField";
 import { DocumentManager } from "../../util/DocumentManager";
 import request = require("request");
 import { ServerUtils } from "../../../server/ServerUtil";
+import { Server } from "../../Server";
 
 export interface CollectionViewProps {
     fieldKey: Key;
@@ -59,17 +60,20 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         let email = CurrentUserUtils.email;
         if (id && email) {
             let textInfo: [string, string] = [id, email];
-            doc.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, field => {
-                let cursors = field.Data;
-                if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
-                    cursors[ind].Data[1] = position;
-                } else {
-                    let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
-                    cursors.push(entry);
+            doc.GetTAsync(KeyStore.Prototype, Document).then(proto => {
+                if (!proto) {
+                    return;
                 }
+                proto.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, action((field: ListField<CursorEntry>) => {
+                    let cursors = field.Data;
+                    if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
+                        cursors[ind].Data[1] = position;
+                    } else {
+                        let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
+                        cursors.push(entry);
+                    }
+                }))
             })
-
-
         }
     }
 
@@ -111,6 +115,21 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
             ctor = Documents.PdfDocument;
         }
         if (type.indexOf("html") !== -1) {
+            if (path.includes('localhost')) {
+                let s = path.split('/');
+                let id = s[s.length - 1];
+                Server.GetField(id).then(field => {
+                    if (field instanceof Document) {
+                        let alias = field.CreateAlias();
+                        alias.SetNumber(KeyStore.X, options.x || 0);
+                        alias.SetNumber(KeyStore.Y, options.y || 0);
+                        alias.SetNumber(KeyStore.Width, options.width || 300);
+                        alias.SetNumber(KeyStore.Height, options.height || options.width || 300);
+                        this.props.addDocument(alias, false);
+                    }
+                })
+                return undefined;
+            }
             ctor = Documents.WebDocument;
             options = { height: options.width, ...options, };
         }
@@ -130,7 +149,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         e.stopPropagation()
         e.preventDefault()
 
-        if (html && html.indexOf("<img") != 0) {
+        if (html && html.indexOf("<img") != 0 && !html.startsWith("<a")) {
             console.log("not good");
             let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 });
             htmlDoc.SetText(KeyStore.DocumentText, text);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index cfe67c8fe..55aa1ccab 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -18,6 +18,7 @@ import { ContextMenu } from "../ContextMenu";
 import { DocumentContentsView } from "./DocumentContentsView";
 import "./DocumentView.scss";
 import React = require("react");
+import { ServerUtils } from "../../../server/ServerUtil";
 
 
 export interface DocumentViewProps {
@@ -283,6 +284,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked })
         ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) })
         ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) })
+        ContextMenu.Instance.addItem({
+            description: "Copy URL",
+            event: () => {
+                Utils.CopyText(ServerUtils.prepend("/doc/" + this.props.Document.Id));
+            }
+        });
         ContextMenu.Instance.addItem({
             description: "Copy ID",
             event: () => {
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index f6343c631..4e83ec7b9 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -16,6 +16,9 @@ import { VideoBox } from "./VideoBox";
 import { AudioBox } from "./AudioBox";
 import { AudioField } from "../../../fields/AudioField";
 import { ListField } from "../../../fields/ListField";
+import { DocumentContentsView } from "./DocumentContentsView";
+import { Transform } from "../../util/Transform";
+import { KeyStore } from "../../../fields/KeyStore";
 
 
 //
@@ -65,7 +68,20 @@ export class FieldView extends React.Component<FieldViewProps> {
             return <AudioBox {...this.props} />
         }
         else if (field instanceof Document) {
-            return <div>{field.Title}</div>
+            return (<DocumentContentsView Document={field}
+                AddDocument={undefined}
+                RemoveDocument={undefined}
+                ScreenToLocalTransform={() => Transform.Identity}
+                ContentScaling={() => 1}
+                PanelWidth={() => 100}
+                PanelHeight={() => 100}
+                isTopMost={true}
+                SelectOnLoad={false}
+                focus={() => { }}
+                isSelected={() => false}
+                select={() => false}
+                layoutKey={KeyStore.Layout}
+                ContainingCollectionView={undefined} />)
         }
         else if (field instanceof ListField) {
             return (<div>
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index ba9bd9566..30fa1342e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -167,6 +167,8 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {
             onKeyPress={this.onKeyPress}
             onPointerDown={this.onPointerDown}
             onContextMenu={this.specificContextMenu}
+            // tfs: do we need this event handler
+            onKeyDown={this.onKeyPress}
             onWheel={this.onPointerWheel}
             ref={this._ref} />)
     }
-- 
cgit v1.2.3-70-g09d2

