From 1bd678851632bbbb302363574eb5e3e19dc343e9 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 29 Mar 2019 13:18:35 -0400
Subject: reorganized and mostly working northstar histograms.

---
 src/client/northstar/dash-nodes/HistogramBox.tsx | 161 +++++++++++++++++++++++
 1 file changed, 161 insertions(+)
 create mode 100644 src/client/northstar/dash-nodes/HistogramBox.tsx

(limited to 'src/client/northstar/dash-nodes/HistogramBox.tsx')

diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
new file mode 100644
index 000000000..9f8c2cfd0
--- /dev/null
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -0,0 +1,161 @@
+import React = require("react")
+import { action, computed, observable, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import Measure from "react-measure";
+import { Dictionary } from "typescript-collections";
+import { FieldWaiting, Opt } from "../../../fields/Field";
+import { KeyStore } from "../../../fields/KeyStore";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { FilterModel } from '../../northstar/core/filter/FilterModel';
+import { ChartType, VisualBinRange } from '../../northstar/model/binRanges/VisualBinRange';
+import { VisualBinRangeHelper } from "../../northstar/model/binRanges/VisualBinRangeHelper";
+import { AggregateBinRange, BinRange, DoubleValueAggregateResult, HistogramResult, Catalog } from "../../northstar/model/idea/idea";
+import { ModelHelpers } from "../../northstar/model/ModelHelpers";
+import { HistogramOperation } from "../../northstar/operations/HistogramOperation";
+import { PIXIRectangle } from "../../northstar/utils/MathUtil";
+import { SizeConverter } from "../../northstar/utils/SizeConverter";
+import { DragManager } from "../../util/DragManager";
+import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
+import { HistogramField } from "../dash-fields/HistogramField";
+import "../utils/Extensions"
+import "./HistogramBox.scss";
+import { HistogramBoxPrimitives } from './HistogramBoxPrimitives';
+import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives";
+
+export interface HistogramPrimitivesProps {
+    HistoBox: HistogramBox;
+}
+
+@observer
+export class HistogramBox extends React.Component<FieldViewProps> {
+    public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(HistogramBox, fieldStr) }
+    private _dropXRef = React.createRef<HTMLDivElement>();
+    private _dropYRef = React.createRef<HTMLDivElement>();
+    private _dropXDisposer?: DragManager.DragDropDisposer;
+    private _dropYDisposer?: DragManager.DragDropDisposer;
+
+    @observable public PanelWidth: number = 100;
+    @observable public PanelHeight: number = 100;
+    @observable public HistoOp: HistogramOperation = HistogramOperation.Empty;
+    @observable public VisualBinRanges: VisualBinRange[] = [];
+    @observable public ValueRange: number[] = [];
+    @observable public SizeConverter: SizeConverter = new SizeConverter();
+
+    @computed get createOperationParamsCache() { return this.HistoOp.CreateOperationParameters(); }
+    @computed get HistogramResult() { return this.HistoOp ? this.HistoOp.Result as HistogramResult : undefined; }
+    @computed get BinRanges() { return this.HistogramResult ? this.HistogramResult.binRanges : undefined; }
+    @computed get ChartType() {
+        return !this.BinRanges ? ChartType.SinglePoint : this.BinRanges[0] instanceof AggregateBinRange ?
+            (this.BinRanges[1] instanceof AggregateBinRange ? ChartType.SinglePoint : ChartType.HorizontalBar) :
+            this.BinRanges[1] instanceof AggregateBinRange ? ChartType.VerticalBar : ChartType.HeatMap;
+    }
+
+    constructor(props: FieldViewProps) {
+        super(props);
+    }
+
+    @action
+    dropX = (e: Event, de: DragManager.DropEvent) => {
+        if (de.data instanceof DragManager.DocumentDragData) {
+            let h = de.data.draggedDocument.GetT(KeyStore.Data, HistogramField);
+            if (h && h != FieldWaiting) {
+                this.HistoOp.X = h.Data.X;
+            }
+            e.stopPropagation();
+            e.preventDefault();
+        }
+    }
+    @action
+    dropY = (e: Event, de: DragManager.DropEvent) => {
+        if (de.data instanceof DragManager.DocumentDragData) {
+            let h = de.data.draggedDocument.GetT(KeyStore.Data, HistogramField);
+            if (h && h != FieldWaiting) {
+                this.HistoOp.Y = h.Data.X;
+            }
+            e.stopPropagation();
+            e.preventDefault();
+        }
+    }
+
+    componentDidMount() {
+        if (this._dropXRef.current) {
+            this._dropXDisposer = DragManager.MakeDropTarget(this._dropXRef.current, { handlers: { drop: this.dropX.bind(this) } });
+        }
+        if (this._dropYRef.current) {
+            this._dropYDisposer = DragManager.MakeDropTarget(this._dropYRef.current, { handlers: { drop: this.dropY.bind(this) } });
+        }
+        reaction(() => CurrentUserUtils.NorthstarDBCatalog, (catalog?: Catalog) => this.activateHistogramOperation(catalog), { fireImmediately: true });
+        reaction(() => [this.VisualBinRanges && this.VisualBinRanges.slice()], () => this.SizeConverter.SetVisualBinRanges(this.VisualBinRanges));
+        reaction(() => [this.PanelHeight, this.PanelWidth], () => this.SizeConverter.SetIsSmall(this.PanelWidth < 40 && this.PanelHeight < 40))
+        reaction(() => this.HistogramResult ? this.HistogramResult.binRanges : undefined,
+            (binRanges: BinRange[] | undefined) => {
+                if (binRanges) {
+                    this.VisualBinRanges.splice(0, this.VisualBinRanges.length, ...binRanges.map((br, ind) =>
+                        VisualBinRangeHelper.GetVisualBinRange(this.HistoOp.Schema!.distinctAttributeParameters, br, this.HistogramResult!, ind ? this.HistoOp.Y : this.HistoOp.X, this.ChartType)));
+
+                    let valueAggregateKey = ModelHelpers.CreateAggregateKey(this.HistoOp.Schema!.distinctAttributeParameters, this.HistoOp.V, this.HistogramResult!, ModelHelpers.AllBrushIndex(this.HistogramResult!));
+                    this.ValueRange = Object.values(this.HistogramResult!.bins!).reduce((prev, cur) => {
+                        let value = ModelHelpers.GetAggregateResult(cur, valueAggregateKey) as DoubleValueAggregateResult;
+                        return value && value.hasResult ? [Math.min(prev[0], value.result!), Math.max(prev[1], value.result!)] : prev;
+                    }, [Number.MAX_VALUE, Number.MIN_VALUE]);
+                }
+            });
+    }
+
+    @action
+    xLabelPointerDown = (e: React.PointerEvent) => {
+
+    }
+
+    componentWillUnmount() {
+        if (this._dropXDisposer)
+            this._dropXDisposer();
+        if (this._dropYDisposer)
+            this._dropYDisposer();
+    }
+
+    activateHistogramOperation(catalog?: Catalog) {
+        if (catalog) {
+            this.props.doc.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt<HistogramField>) => runInAction(() => {
+                this.HistoOp = histoOp ? histoOp.Data : HistogramOperation.Empty;
+                if (this.HistoOp != HistogramOperation.Empty) {
+                    reaction(() => this.props.doc.GetList(KeyStore.LinkedFromDocs, []), docs => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
+                    reaction(() => this.createOperationParamsCache, () => this.HistoOp.Update(), { fireImmediately: true });
+                }
+            }));
+        }
+    }
+    render() {
+        let labelY = this.HistoOp && this.HistoOp.Y ? this.HistoOp.Y.PresentedName : "<...>";
+        let labelX = this.HistoOp && this.HistoOp.X ? this.HistoOp.X.PresentedName : "<...>";
+        var h = this.props.isTopMost ? this.PanelHeight : this.props.doc.GetNumber(KeyStore.Height, 0);
+        var w = this.props.isTopMost ? this.PanelWidth : this.props.doc.GetNumber(KeyStore.Width, 0);
+        let loff = this.SizeConverter.LeftOffset;
+        let toff = this.SizeConverter.TopOffset;
+        let roff = this.SizeConverter.RightOffset;
+        let boff = this.SizeConverter.BottomOffset;
+        return (
+            <Measure onResize={(r: any) => runInAction(() => { this.PanelWidth = r.entry.width; this.PanelHeight = r.entry.height })}>
+                {({ measureRef }) =>
+                    <div className="histogrambox-container" ref={measureRef} style={{ transform: `translate(${-w / 2}px, ${-h / 2}px)` }}>
+                        <div className="histogrambox-yaxislabel" ref={this._dropYRef} >
+                            <span className="histogrambox-yaxislabel-text">
+                                {labelY}
+                            </span>
+                        </div>
+                        <div style={{
+                            transform: `translate(${loff + 25}px, ${toff}px)`,
+                            width: `calc(100% - ${loff + roff + 25}px)`,
+                            height: `calc(100% - ${toff + boff}px)`,
+                        }}>
+                            <HistogramLabelPrimitives HistoBox={this} />
+                            <HistogramBoxPrimitives HistoBox={this} />
+                        </div>
+                        <div className="histogrambox-xaxislabel" onPointerDown={this.xLabelPointerDown} ref={this._dropXRef} >{labelX}</div>
+                    </div>
+                }
+            </Measure>
+        )
+    }
+}
+
-- 
cgit v1.2.3-70-g09d2


From da7a12f9b49b43534658524b1da846948fbf3947 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 29 Mar 2019 14:37:26 -0400
Subject: fixed some bugs added some small features to histograms.

---
 src/client/Server.ts                                 |  4 ++--
 src/client/documents/Documents.ts                    |  4 ++--
 src/client/northstar/dash-nodes/HistogramBox.tsx     | 17 ++++++++++++-----
 .../northstar/dash-nodes/HistogramBoxPrimitives.tsx  |  5 ++++-
 src/client/northstar/utils/SizeConverter.ts          |  2 +-
 src/client/util/DragManager.ts                       |  2 ++
 src/client/views/Main.tsx                            |  6 +++---
 .../collectionFreeForm/CollectionFreeFormView.scss   | 20 ++++++++++----------
 8 files changed, 36 insertions(+), 24 deletions(-)

(limited to 'src/client/northstar/dash-nodes/HistogramBox.tsx')

diff --git a/src/client/Server.ts b/src/client/Server.ts
index feafe9eb4..37e3c2c0d 100644
--- a/src/client/Server.ts
+++ b/src/client/Server.ts
@@ -75,7 +75,7 @@ export class Server {
                     existingFields[id] = field;
                 }
             }
-            SocketStub.SEND_FIELDS_REQUEST(neededFieldIds, (fields) => {
+            SocketStub.SEND_FIELDS_REQUEST(neededFieldIds, action((fields: FieldMap) => {
                 for (let id of neededFieldIds) {
                     let field = fields[id];
                     if (field) {
@@ -104,7 +104,7 @@ export class Server {
                         cb({ ...fields, ...existingFields })
                     }
                 }, { fireImmediately: true })
-            });
+            }));
         };
         if (callback) {
             fn(callback);
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 663ccae61..0bf275df8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -190,8 +190,8 @@ export namespace Documents {
         return assignToDelegate(SetInstanceOptions(GetAudioPrototype(), options, [new URL(url), AudioField]), options);
     }
 
-    export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}, id?: string) {
-        return assignToDelegate(SetInstanceOptions(GetHistogramPrototype(), options, [histoOp, HistogramField], id).MakeDelegate(), options);
+    export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}, id?: string, delegId?: string) {
+        return assignToDelegate(SetInstanceOptions(GetHistogramPrototype(), options, [histoOp, HistogramField], id).MakeDelegate(delegId), options);
     }
     export function TextDocument(options: DocumentOptions = {}) {
         return assignToDelegate(SetInstanceOptions(GetTextPrototype(), options, ["", TextField]).MakeDelegate(), options);
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index 9f8c2cfd0..dba4ce900 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -9,7 +9,7 @@ import { CurrentUserUtils } from "../../../server/authentication/models/current_
 import { FilterModel } from '../../northstar/core/filter/FilterModel';
 import { ChartType, VisualBinRange } from '../../northstar/model/binRanges/VisualBinRange';
 import { VisualBinRangeHelper } from "../../northstar/model/binRanges/VisualBinRangeHelper";
-import { AggregateBinRange, BinRange, DoubleValueAggregateResult, HistogramResult, Catalog } from "../../northstar/model/idea/idea";
+import { AggregateBinRange, BinRange, DoubleValueAggregateResult, HistogramResult, Catalog, AggregateFunction } from "../../northstar/model/idea/idea";
 import { ModelHelpers } from "../../northstar/model/ModelHelpers";
 import { HistogramOperation } from "../../northstar/operations/HistogramOperation";
 import { PIXIRectangle } from "../../northstar/utils/MathUtil";
@@ -21,6 +21,7 @@ import "../utils/Extensions"
 import "./HistogramBox.scss";
 import { HistogramBoxPrimitives } from './HistogramBoxPrimitives';
 import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives";
+import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
 
 export interface HistogramPrimitivesProps {
     HistoBox: HistogramBox;
@@ -104,7 +105,11 @@ export class HistogramBox extends React.Component<FieldViewProps> {
 
     @action
     xLabelPointerDown = (e: React.PointerEvent) => {
-
+        this.HistoOp.X = new AttributeTransformationModel(this.HistoOp.X.AttributeModel, this.HistoOp.X.AggregateFunction == AggregateFunction.None ? AggregateFunction.Count : AggregateFunction.None);
+    }
+    @action
+    yLabelPointerDown = (e: React.PointerEvent) => {
+        this.HistoOp.Y = new AttributeTransformationModel(this.HistoOp.Y.AttributeModel, this.HistoOp.Y.AggregateFunction == AggregateFunction.None ? AggregateFunction.Count : AggregateFunction.None);
     }
 
     componentWillUnmount() {
@@ -138,12 +143,12 @@ export class HistogramBox extends React.Component<FieldViewProps> {
             <Measure onResize={(r: any) => runInAction(() => { this.PanelWidth = r.entry.width; this.PanelHeight = r.entry.height })}>
                 {({ measureRef }) =>
                     <div className="histogrambox-container" ref={measureRef} style={{ transform: `translate(${-w / 2}px, ${-h / 2}px)` }}>
-                        <div className="histogrambox-yaxislabel" ref={this._dropYRef} >
+                        <div className="histogrambox-yaxislabel" onPointerDown={this.yLabelPointerDown} ref={this._dropYRef} >
                             <span className="histogrambox-yaxislabel-text">
                                 {labelY}
                             </span>
                         </div>
-                        <div style={{
+                        <div className="histogrambox-primitives" style={{
                             transform: `translate(${loff + 25}px, ${toff}px)`,
                             width: `calc(100% - ${loff + roff + 25}px)`,
                             height: `calc(100% - ${toff + boff}px)`,
@@ -151,7 +156,9 @@ export class HistogramBox extends React.Component<FieldViewProps> {
                             <HistogramLabelPrimitives HistoBox={this} />
                             <HistogramBoxPrimitives HistoBox={this} />
                         </div>
-                        <div className="histogrambox-xaxislabel" onPointerDown={this.xLabelPointerDown} ref={this._dropXRef} >{labelX}</div>
+                        <div className="histogrambox-xaxislabel" onPointerDown={this.xLabelPointerDown} ref={this._dropXRef} >
+                            {labelX}
+                        </div>
                     </div>
                 }
             </Measure>
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
index d2f1be4fd..5e403eb9c 100644
--- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
@@ -1,5 +1,5 @@
 import React = require("react")
-import { computed, observable, runInAction } from "mobx";
+import { computed, observable, runInAction, reaction } from "mobx";
 import { observer } from "mobx-react";
 import { Utils as DashUtils } from '../../../Utils';
 import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel";
@@ -19,6 +19,9 @@ import "./HistogramBoxPrimitives.scss";
 export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesProps> {
     private get histoOp() { return this.props.HistoBox.HistoOp; }
     private get renderDimension() { return this.props.HistoBox.SizeConverter.RenderDimension; }
+    componentDidMount() {
+        reaction(() => this.props.HistoBox.HistogramResult, () => this._selectedPrims.length = 0);
+    }
     @observable _selectedPrims: HistogramBinPrimitive[] = [];
     @computed get xaxislines() { return this.renderGridLinesAndLabels(0); }
     @computed get yaxislines() { return this.renderGridLinesAndLabels(1); }
diff --git a/src/client/northstar/utils/SizeConverter.ts b/src/client/northstar/utils/SizeConverter.ts
index 30627dfd5..bb91ed4a7 100644
--- a/src/client/northstar/utils/SizeConverter.ts
+++ b/src/client/northstar/utils/SizeConverter.ts
@@ -62,7 +62,7 @@ export class SizeConverter {
 
     public DataToScreenPointRange(axis: number, bin: Bin, aggregateKey: AggregateKey) {
         var value = ModelHelpers.GetAggregateResult(bin, aggregateKey) as DoubleValueAggregateResult;
-        if (value.hasResult)
+        if (value && value.hasResult)
             return [this.DataToScreenCoord(value.result!, axis) - 5,
             this.DataToScreenCoord(value.result!, axis) + 5];
         return [undefined, undefined];
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 661fa4dc8..70b1c9829 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -186,6 +186,8 @@ export namespace DragManager {
             e.preventDefault();
             x += e.movementX;
             y += e.movementY;
+            if (dragData instanceof DocumentDragData)
+                dragData.aliasOnDrop = e.ctrlKey || e.altKey;
             if (e.shiftKey) {
                 abortDrag();
                 CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 });
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 2f20b102c..75855c3d1 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -245,7 +245,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 addTreeNode = action(() => Documents.TreeDocument(this._northstarSchemas, { width: 100, height: 400, title: "northstar schemas" }));
+        let addTreeNode = action(() => Documents.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas" }));
         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" }));
@@ -340,7 +340,7 @@ export class Main extends React.Component {
                 let schemaDoc = Documents.TreeDocument([], { width: 50, height: 100, title: schema.displayName! });
                 let schemaDocuments = schemaDoc.GetList(KeyStore.Data, [] as Document[]);
                 CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
-                    Server.GetField(attr.displayName!, action((field: Opt<Field>) => {
+                    Server.GetField(attr.displayName! + ".alias", action((field: Opt<Field>) => {
                         if (field instanceof Document) {
                             schemaDocuments.push(field);
                         } else {
@@ -349,7 +349,7 @@ export class Main extends React.Component {
                                 new AttributeTransformationModel(atmod, AggregateFunction.None),
                                 new AttributeTransformationModel(atmod, AggregateFunction.Count),
                                 new AttributeTransformationModel(atmod, AggregateFunction.Count));
-                            schemaDocuments.push(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, attr.displayName!));
+                            schemaDocuments.push(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias"));
                         }
                     }));
                 });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 9c5e98005..215a75243 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -1,5 +1,15 @@
 @import "../../global_variables";
 
+.collectionfreeformview {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  .inking-canvas {
+    transform-origin: 50000px 50000px;
+  }
+}
 .collectionfreeformview-container {
   .collectionfreeformview > .jsx-parser {
     position: absolute;
@@ -27,16 +37,6 @@
   left: 0;
   width: 100%;
   height: 100%;
-  .collectionfreeformview {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    .inking-canvas {
-      transform-origin: 50000px 50000px;
-    }
-  }
 }
 .collectionfreeformview-overlay {
   .collectionfreeformview > .jsx-parser {
-- 
cgit v1.2.3-70-g09d2


From a10bbd686a825ee38caceb7ecfc388715c14a817 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 29 Mar 2019 16:18:55 -0400
Subject: added basic brushing placeholder

---
 .../northstar/core/brusher/BrushLinkModel.ts       | 40 ------------------
 .../northstar/core/brusher/IBaseBrushable.ts       |  4 +-
 src/client/northstar/dash-nodes/HistogramBox.tsx   | 19 +++++----
 .../dash-nodes/HistogramBoxPrimitives.tsx          |  5 ++-
 .../northstar/operations/HistogramOperation.ts     | 48 +++++++++++-----------
 5 files changed, 43 insertions(+), 73 deletions(-)
 delete mode 100644 src/client/northstar/core/brusher/BrushLinkModel.ts

(limited to 'src/client/northstar/dash-nodes/HistogramBox.tsx')

diff --git a/src/client/northstar/core/brusher/BrushLinkModel.ts b/src/client/northstar/core/brusher/BrushLinkModel.ts
deleted file mode 100644
index e3ac43367..000000000
--- a/src/client/northstar/core/brusher/BrushLinkModel.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-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
index 07d4e7580..99a36636f 100644
--- a/src/client/northstar/core/brusher/IBaseBrushable.ts
+++ b/src/client/northstar/core/brusher/IBaseBrushable.ts
@@ -1,9 +1,9 @@
-import { BrushLinkModel } from '../brusher/BrushLinkModel'
 import { PIXIPoint } from '../../utils/MathUtil'
 import { IEquatable } from '../../utils/IEquatable';
+import { Document } from '../../../../fields/Document'
 
 export interface IBaseBrushable<T> extends IEquatable {
-    BrusherModels: Array<BrushLinkModel<T>>;
+    BrusherModels: Array<Document>;
     BrushColors: Array<number>;
     Position: PIXIPoint;
     Size: PIXIPoint;
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index dba4ce900..b464e125c 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -2,26 +2,25 @@ import React = require("react")
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import Measure from "react-measure";
-import { Dictionary } from "typescript-collections";
 import { FieldWaiting, Opt } from "../../../fields/Field";
+import { Document } from "../../../fields/Document";
 import { KeyStore } from "../../../fields/KeyStore";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { FilterModel } from '../../northstar/core/filter/FilterModel';
 import { ChartType, VisualBinRange } from '../../northstar/model/binRanges/VisualBinRange';
 import { VisualBinRangeHelper } from "../../northstar/model/binRanges/VisualBinRangeHelper";
-import { AggregateBinRange, BinRange, DoubleValueAggregateResult, HistogramResult, Catalog, AggregateFunction } from "../../northstar/model/idea/idea";
+import { AggregateBinRange, AggregateFunction, BinRange, Catalog, DoubleValueAggregateResult, HistogramResult } from "../../northstar/model/idea/idea";
 import { ModelHelpers } from "../../northstar/model/ModelHelpers";
 import { HistogramOperation } from "../../northstar/operations/HistogramOperation";
-import { PIXIRectangle } from "../../northstar/utils/MathUtil";
 import { SizeConverter } from "../../northstar/utils/SizeConverter";
 import { DragManager } from "../../util/DragManager";
 import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
+import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
 import { HistogramField } from "../dash-fields/HistogramField";
-import "../utils/Extensions"
+import "../utils/Extensions";
 import "./HistogramBox.scss";
 import { HistogramBoxPrimitives } from './HistogramBoxPrimitives';
 import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives";
-import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
+import { StyleConstants } from "../utils/StyleContants";
 
 export interface HistogramPrimitivesProps {
     HistoBox: HistogramBox;
@@ -124,7 +123,13 @@ export class HistogramBox extends React.Component<FieldViewProps> {
             this.props.doc.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt<HistogramField>) => runInAction(() => {
                 this.HistoOp = histoOp ? histoOp.Data : HistogramOperation.Empty;
                 if (this.HistoOp != HistogramOperation.Empty) {
-                    reaction(() => this.props.doc.GetList(KeyStore.LinkedFromDocs, []), docs => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
+                    reaction(() => this.props.doc.GetList(KeyStore.LinkedFromDocs, []),
+                        (docs: Document[]) => {
+                            var availableColors = StyleConstants.BRUSH_COLORS.map(c => c);
+                            docs.map((brush, i) => brush.SetNumber(KeyStore.BackgroundColor, availableColors[i % availableColors.length]));
+                            this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...docs);
+                            //this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs)
+                        }, { fireImmediately: true });
                     reaction(() => this.createOperationParamsCache, () => this.HistoOp.Update(), { fireImmediately: true });
                 }
             }));
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
index 5e403eb9c..b8020c28c 100644
--- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
@@ -168,7 +168,7 @@ export class HistogramBinPrimitiveCollection {
         var filteredBinPrims = this.BinPrimitives.filter(b => b.BrushIndex != allBrushIndex && b.DataValue != 0.0);
         filteredBinPrims.reduce((sum, fbp) => {
             if (histoBox.ChartType == ChartType.VerticalBar) {
-                if (this.histoOp.X.AggregateFunction == AggregateFunction.Count) {
+                if (this.histoOp.Y.AggregateFunction == AggregateFunction.Count) {
                     fbp.Rect = new PIXIRectangle(fbp.Rect.x, fbp.Rect.y - sum, fbp.Rect.width, fbp.Rect.height);
                     fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x, fbp.MarginRect.y - sum, fbp.MarginRect.width, fbp.MarginRect.height);
                     return sum + fbp.Rect.height;
@@ -269,6 +269,9 @@ export class HistogramBinPrimitiveCollection {
     private createVerticalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number): number {
         let dataValue = this.histoOp.getValue(1, bin, this.histoResult, brush.brushIndex!);
         if (dataValue != undefined) {
+            if (bin.binIndex!.indices![0] == 0 && bin.binIndex!.indices![1] == 0) {
+                console.log("DV = " + dataValue)
+            }
             let [yFrom, yValue, yTo] = this.sizeConverter.DataToScreenNormalizedRange(dataValue, normalization, 1, binBrushMaxAxis);
             let [xFrom, xTo] = this.sizeConverter.DataToScreenXAxisRange(this._histoBox.VisualBinRanges, 0, bin);
 
diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
index 8a0f648f6..fa51e2a8c 100644
--- a/src/client/northstar/operations/HistogramOperation.ts
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -4,29 +4,30 @@ import { CurrentUserUtils } from "../../../server/authentication/models/current_
 import { ColumnAttributeModel } from "../core/attribute/AttributeModel";
 import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
 import { CalculatedAttributeManager } from "../core/attribute/CalculatedAttributeModel";
-import { BrushLinkModel } from "../core/brusher/BrushLinkModel";
 import { FilterModel } from "../core/filter/FilterModel";
 import { FilterOperand } from "../core/filter/FilterOperand";
 import { IBaseFilterConsumer } from "../core/filter/IBaseFilterConsumer";
-import { IBaseFilterProvider } from "../core/filter/IBaseFilterProvider";
+import { IBaseFilterProvider, instanceOfIBaseFilterProvider } from "../core/filter/IBaseFilterProvider";
 import { SETTINGS_SAMPLE_SIZE, SETTINGS_X_BINS, SETTINGS_Y_BINS } from "../model/binRanges/VisualBinRangeHelper";
 import { AggregateFunction, AggregateParameters, Attribute, AverageAggregateParameters, DataType, HistogramOperationParameters, QuantitativeBinRange, HistogramResult, Brush, DoubleValueAggregateResult, Bin } from "../model/idea/idea";
 import { ModelHelpers } from "../model/ModelHelpers";
 import { ArrayUtil } from "../utils/ArrayUtil";
 import { BaseOperation } from "./BaseOperation";
+import { KeyStore } from "../../../fields/KeyStore";
+import { HistogramField } from "../dash-fields/HistogramField";
+import { FieldWaiting } from "../../../fields/Field";
 
 
 export class HistogramOperation extends BaseOperation implements IBaseFilterConsumer, IBaseFilterProvider {
     @observable public FilterOperand: FilterOperand = FilterOperand.AND;
     @observable public Links: Document[] = [];
+    @observable public BrushLinks: Document[] = [];
     @observable public BrushColors: number[] = [];
     @observable public Normalization: number = -1;
     @observable public FilterModels: FilterModel[] = [];
     @observable public X: AttributeTransformationModel;
     @observable public Y: AttributeTransformationModel;
     @observable public V: AttributeTransformationModel;
-    @observable public BrusherModels: BrushLinkModel<HistogramOperation>[] = [];
-    @observable public BrushableModels: BrushLinkModel<HistogramOperation>[] = [];
     @observable public SchemaName: string;
     @computed public get Schema() { return CurrentUserUtils.GetNorthstarSchema(this.SchemaName); }
 
@@ -63,25 +64,24 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
     @computed
     public get FilterString(): string {
         let filterModels: FilterModel[] = [];
-        let fstring = FilterModel.GetFilterModelsRecursive(this, new Set<IBaseFilterProvider>(), filterModels, true)
-        return fstring;
+        return FilterModel.GetFilterModelsRecursive(this, new Set<IBaseFilterProvider>(), filterModels, true)
     }
 
-    @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
+    public get BrushString(): string[] {
+        let brushes: string[] = [];
+        this.BrushLinks.map(brushLink => {
+            let brusherDoc = brushLink.Get(KeyStore.LinkedFromDocs);
+            if (brusherDoc && brusherDoc != FieldWaiting && brusherDoc instanceof Document) {
+                let brushHistogram = brusherDoc.GetT(KeyStore.Data, HistogramField);
+                if (brushHistogram && brushHistogram != FieldWaiting) {
+                    let filterModels: FilterModel[] = [];
+                    let brush = FilterModel.GetFilterModelsRecursive(brushHistogram!.Data, new Set<IBaseFilterProvider>(), filterModels, false)
+                    brushes.push(brush);
+                }
+            }
+        });
+        return brushes;
     }
 
 
@@ -131,11 +131,13 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
             });
         }
     }
-
+    get_random_color(): number {
+        return (Math.floor(Math.random() * 256) << 16) + (Math.floor(Math.random() * 256) << 8) + (Math.floor(Math.random() * 256));
+    }
 
     @action
     public async Update(): Promise<void> {
-        this.BrushColors = this.BrusherModels.map(e => e.Color);
+        this.BrushColors = this.BrushLinks.map(e => e.GetNumber(KeyStore.BackgroundColor, 0));
         return super.Update();
     }
 }
-- 
cgit v1.2.3-70-g09d2


From 6e993fb5817e8ddce756396e53883a42530f52bb Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 29 Mar 2019 18:49:22 -0400
Subject: brushes mostly working - some problems with cycles.

---
 src/client/northstar/dash-fields/HistogramField.ts | 17 +++++++-
 src/client/northstar/dash-nodes/HistogramBox.tsx   | 27 ++++++++----
 .../dash-nodes/HistogramBoxPrimitives.tsx          | 14 ++++---
 src/client/northstar/operations/BaseOperation.ts   |  6 ++-
 .../northstar/operations/HistogramOperation.ts     | 22 +++++-----
 .../CollectionFreeFormLinksView.tsx                | 49 +++++++++++++++++++++-
 src/fields/KeyStore.ts                             |  1 +
 7 files changed, 107 insertions(+), 29 deletions(-)

(limited to 'src/client/northstar/dash-nodes/HistogramBox.tsx')

diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts
index 00912c595..1929f8dcd 100644
--- a/src/client/northstar/dash-fields/HistogramField.ts
+++ b/src/client/northstar/dash-fields/HistogramField.ts
@@ -16,7 +16,8 @@ export class HistogramField extends BasicField<HistogramOperation> {
     }
 
     toString(): string {
-        return JSON.stringify(this.Data);
+        let omitted = this.omitKeys(this.Data, ['Links', 'BrushLinks']);
+        return JSON.stringify(omitted);
     }
 
     Copy(): Field {
@@ -27,10 +28,22 @@ export class HistogramField extends BasicField<HistogramOperation> {
         return `new HistogramField("${this.Data}")`;
     }
 
+    omitKeys(obj: any, keys: any) {
+        var dup: any = {};
+        for (var key in obj) {
+            if (keys.indexOf(key) == -1) {
+                dup[key] = obj[key];
+            }
+        }
+        return dup;
+    }
+
     ToJson(): { type: Types, data: string, _id: string } {
+        let omitted = this.omitKeys(this.Data, ['Links', 'BrushLinks']);
         return {
             type: Types.HistogramOp,
-            data: JSON.stringify(this.Data),
+
+            data: JSON.stringify(omitted),
             _id: this.Id
         }
     }
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index b464e125c..9976ff6ad 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -1,5 +1,5 @@
 import React = require("react")
-import { action, computed, observable, reaction, runInAction } from "mobx";
+import { action, computed, observable, reaction, runInAction, trace } from "mobx";
 import { observer } from "mobx-react";
 import Measure from "react-measure";
 import { FieldWaiting, Opt } from "../../../fields/Field";
@@ -8,7 +8,7 @@ import { KeyStore } from "../../../fields/KeyStore";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 import { ChartType, VisualBinRange } from '../../northstar/model/binRanges/VisualBinRange';
 import { VisualBinRangeHelper } from "../../northstar/model/binRanges/VisualBinRangeHelper";
-import { AggregateBinRange, AggregateFunction, BinRange, Catalog, DoubleValueAggregateResult, HistogramResult } from "../../northstar/model/idea/idea";
+import { AggregateBinRange, AggregateFunction, BinRange, Catalog, DoubleValueAggregateResult, HistogramResult, Result } from "../../northstar/model/idea/idea";
 import { ModelHelpers } from "../../northstar/model/ModelHelpers";
 import { HistogramOperation } from "../../northstar/operations/HistogramOperation";
 import { SizeConverter } from "../../northstar/utils/SizeConverter";
@@ -39,10 +39,10 @@ export class HistogramBox extends React.Component<FieldViewProps> {
     @observable public HistoOp: HistogramOperation = HistogramOperation.Empty;
     @observable public VisualBinRanges: VisualBinRange[] = [];
     @observable public ValueRange: number[] = [];
+    @observable public HistogramResult: HistogramResult = new HistogramResult();
     @observable public SizeConverter: SizeConverter = new SizeConverter();
 
-    @computed get createOperationParamsCache() { return this.HistoOp.CreateOperationParameters(); }
-    @computed get HistogramResult() { return this.HistoOp ? this.HistoOp.Result as HistogramResult : undefined; }
+    @computed get createOperationParamsCache() { trace(); return this.HistoOp.CreateOperationParameters(); }
     @computed get BinRanges() { return this.HistogramResult ? this.HistogramResult.binRanges : undefined; }
     @computed get ChartType() {
         return !this.BinRanges ? ChartType.SinglePoint : this.BinRanges[0] instanceof AggregateBinRange ?
@@ -123,12 +123,21 @@ export class HistogramBox extends React.Component<FieldViewProps> {
             this.props.doc.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt<HistogramField>) => runInAction(() => {
                 this.HistoOp = histoOp ? histoOp.Data : HistogramOperation.Empty;
                 if (this.HistoOp != HistogramOperation.Empty) {
-                    reaction(() => this.props.doc.GetList(KeyStore.LinkedFromDocs, []),
-                        (docs: Document[]) => {
+                    this.HistoOp.YieldResult = (r: Result) => action(() => this.HistogramResult = r as HistogramResult)();
+                    reaction(() => this.props.doc.GetList(KeyStore.LinkedFromDocs, []), (docs: Document[]) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
+                    reaction(() => this.props.doc.GetList(KeyStore.BrushingDocs, []).length,
+                        () => {
+                            let brushingDocs = this.props.doc.GetList(KeyStore.BrushingDocs, [] as Document[]);
                             var availableColors = StyleConstants.BRUSH_COLORS.map(c => c);
-                            docs.map((brush, i) => brush.SetNumber(KeyStore.BackgroundColor, availableColors[i % availableColors.length]));
-                            this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...docs);
-                            //this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs)
+                            let proto = this.props.doc.GetPrototype() as Document;
+                            let brushingLinks = brushingDocs.map((brush, i) => {
+                                // brush.SetNumber(KeyStore.BackgroundColor, availableColors[i % availableColors.length]);
+                                let brushed = brush.GetList(KeyStore.BrushingDocs, [] as Document[]);
+                                if (!brushed || brushed.length < 2)
+                                    return undefined;
+                                return { l: brush, b: brushed[0].Id == proto.Id ? brushed[1] : brushed[0] }
+                            }).filter(x => x != undefined) as { l: Document, b: Document }[];
+                            this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingLinks);
                         }, { fireImmediately: true });
                     reaction(() => this.createOperationParamsCache, () => this.HistoOp.Update(), { fireImmediately: true });
                 }
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
index 648070241..c97acb064 100644
--- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
@@ -1,5 +1,5 @@
 import React = require("react")
-import { computed, observable, runInAction, reaction } from "mobx";
+import { computed, observable, runInAction, reaction, untracked, trace } from "mobx";
 import { observer } from "mobx-react";
 import { Utils as DashUtils } from '../../../Utils';
 import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel";
@@ -20,7 +20,7 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
     private get histoOp() { return this.props.HistoBox.HistoOp; }
     private get renderDimension() { return this.props.HistoBox.SizeConverter.RenderDimension; }
     componentDidMount() {
-        reaction(() => this.props.HistoBox.HistogramResult, () => this._selectedPrims.length = 0);
+        reaction(() => this.props.HistoBox.HistoOp.FilterString, () => this._selectedPrims.length = this.histoOp.FilterModels.length = 0);
     }
     @observable _selectedPrims: HistogramBinPrimitive[] = [];
     @computed get xaxislines() { return this.renderGridLinesAndLabels(0); }
@@ -30,10 +30,10 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
         let histoResult = this.props.HistoBox.HistogramResult;
         if (!histoResult || !histoResult.bins || !this.props.HistoBox.VisualBinRanges.length)
             return (null);
+        trace();
         let allBrushIndex = ModelHelpers.AllBrushIndex(histoResult);
         return Object.keys(histoResult.bins).reduce((prims, key) => {
             let drawPrims = new HistogramBinPrimitiveCollection(histoResult!.bins![key], this.props.HistoBox);
-
             let toggle = this.getSelectionToggle(drawPrims.BinPrimitives, allBrushIndex,
                 ModelHelpers.GetBinFilterModel(histoResult!.bins![key], allBrushIndex, histoResult!, this.histoOp.X, this.histoOp.Y));
             drawPrims.BinPrimitives.filter(bp => bp.DataValue && bp.BrushIndex !== allBrushIndex).map(bp =>
@@ -327,6 +327,7 @@ export class HistogramBinPrimitiveCollection {
     }
 
     private baseColorFromBrush(brush: Brush): number {
+        let bc = StyleConstants.BRUSH_COLORS;
         if (brush.brushIndex == ModelHelpers.RestBrushIndex(this.histoResult)) {
             return StyleConstants.HIGHLIGHT_COLOR;
         }
@@ -336,9 +337,12 @@ export class HistogramBinPrimitiveCollection {
         else if (brush.brushIndex == ModelHelpers.AllBrushIndex(this.histoResult)) {
             return 0x00ff00;
         }
-        else if (this.histoOp.BrushColors.length > 0) {
-            return this.histoOp.BrushColors[brush.brushIndex! % this.histoOp.BrushColors.length];
+        else if (bc.length > 0) {
+            return bc[brush.brushIndex! % bc.length];
         }
+        // else if (this.histoOp.BrushColors.length > 0) {
+        //     return this.histoOp.BrushColors[brush.brushIndex! % this.histoOp.BrushColors.length];
+        // }
         return StyleConstants.HIGHLIGHT_COLOR;
     }
 }
\ No newline at end of file
diff --git a/src/client/northstar/operations/BaseOperation.ts b/src/client/northstar/operations/BaseOperation.ts
index 94e0849af..21d1db749 100644
--- a/src/client/northstar/operations/BaseOperation.ts
+++ b/src/client/northstar/operations/BaseOperation.ts
@@ -10,7 +10,8 @@ export abstract class BaseOperation {
 
     @observable public Error: string = "";
     @observable public OverridingFilters: FilterModel[] = [];
-    @observable public Result?: Result = undefined;
+    //@observable 
+    public Result?: Result = undefined;
     @observable public ComputationStarted: boolean = false;
     public OperationReference?: OperationReference = undefined;
 
@@ -45,8 +46,11 @@ export abstract class BaseOperation {
     }
 
 
+    public YieldResult: ((result: Result) => void) | undefined;
     @action
     public SetResult(result: Result): void {
+        if (this.YieldResult)
+            this.YieldResult(result);
         this.Result = result;
     }
 
diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
index fa51e2a8c..48bad73df 100644
--- a/src/client/northstar/operations/HistogramOperation.ts
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -1,4 +1,4 @@
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, trace } from "mobx";
 import { Document } from "../../../fields/Document";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 import { ColumnAttributeModel } from "../core/attribute/AttributeModel";
@@ -16,12 +16,13 @@ import { BaseOperation } from "./BaseOperation";
 import { KeyStore } from "../../../fields/KeyStore";
 import { HistogramField } from "../dash-fields/HistogramField";
 import { FieldWaiting } from "../../../fields/Field";
+import { StyleConstants } from "../utils/StyleContants";
 
 
 export class HistogramOperation extends BaseOperation implements IBaseFilterConsumer, IBaseFilterProvider {
     @observable public FilterOperand: FilterOperand = FilterOperand.AND;
     @observable public Links: Document[] = [];
-    @observable public BrushLinks: Document[] = [];
+    @observable public BrushLinks: { l: Document, b: Document }[] = [];
     @observable public BrushColors: number[] = [];
     @observable public Normalization: number = -1;
     @observable public FilterModels: FilterModel[] = [];
@@ -69,16 +70,15 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
 
     @computed
     public get BrushString(): string[] {
+        trace();
         let brushes: string[] = [];
         this.BrushLinks.map(brushLink => {
-            let brusherDoc = brushLink.Get(KeyStore.LinkedFromDocs);
-            if (brusherDoc && brusherDoc != FieldWaiting && brusherDoc instanceof Document) {
-                let brushHistogram = brusherDoc.GetT(KeyStore.Data, HistogramField);
-                if (brushHistogram && brushHistogram != FieldWaiting) {
-                    let filterModels: FilterModel[] = [];
-                    let brush = FilterModel.GetFilterModelsRecursive(brushHistogram!.Data, new Set<IBaseFilterProvider>(), filterModels, false)
-                    brushes.push(brush);
-                }
+            let brusherDoc = brushLink.b;
+            let brushHistogram = brusherDoc.GetT(KeyStore.Data, HistogramField);
+            if (brushHistogram && brushHistogram != FieldWaiting) {
+                let filterModels: FilterModel[] = [];
+                let brush = FilterModel.GetFilterModelsRecursive(brushHistogram!.Data, new Set<IBaseFilterProvider>(), filterModels, false)
+                brushes.push(brush);
             }
         });
         return brushes;
@@ -137,7 +137,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
 
     @action
     public async Update(): Promise<void> {
-        this.BrushColors = this.BrushLinks.map(e => e.GetNumber(KeyStore.BackgroundColor, 0));
+        //this.BrushColors = this.BrushLinks.map(e => e.l.GetNumber(KeyStore.BackgroundColor, 0));
         return super.Update();
     }
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 4f28f43eb..8dbf80533 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,4 +1,4 @@
-import { computed } from "mobx";
+import { computed, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { Document } from "../../../../fields/Document";
 import { FieldWaiting } from "../../../../fields/Field";
@@ -11,10 +11,57 @@ import "./CollectionFreeFormLinksView.scss";
 import React = require("react");
 import v5 = require("uuid/v5");
 import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
+import { ListField } from "../../../../fields/ListField";
+import { TextField } from "../../../../fields/TextField";
+import { StyleConstants } from "../../../northstar/utils/StyleContants";
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
 
+    componentDidMount() {
+        reaction(() => {
+            return DocumentManager.Instance.getAllDocumentViews(this.props.Document).map(dv => dv.props.Document.GetNumber(KeyStore.X, 0))
+        }, () => {
+            let views = DocumentManager.Instance.getAllDocumentViews(this.props.Document);
+            for (let i = 0; i < views.length; i++) {
+                for (let j = i + 1; j < views.length; j++) {
+                    let srcDoc = views[j].props.Document;
+                    let dstDoc = views[i].props.Document;
+                    let x1 = srcDoc.GetNumber(KeyStore.X, 0);
+                    let x1w = srcDoc.GetNumber(KeyStore.Width, 0);
+                    let x2 = dstDoc.GetNumber(KeyStore.X, 0);
+                    let x2w = dstDoc.GetNumber(KeyStore.Width, 0);
+                    if (Math.abs(x1 + x1w - x2) < 20 || Math.abs(x2 + x2w - x1) < 20) {
+                        let linkDoc: Document = new Document();
+                        dstDoc.GetTAsync(KeyStore.Prototype, Document).then((protoDest) =>
+                            srcDoc.GetTAsync(KeyStore.Prototype, Document).then((protoSrc) => runInAction(() => {
+                                linkDoc.Set(KeyStore.Title, new TextField("New Brush"));
+                                linkDoc.Set(KeyStore.LinkDescription, new TextField(""));
+                                linkDoc.Set(KeyStore.LinkTags, new TextField("Default"));
+                                linkDoc.SetNumber(KeyStore.BackgroundColor, StyleConstants.BRUSH_COLORS[0]);
+
+                                let dstTarg = (protoDest ? protoDest : dstDoc);
+                                let srcTarg = (protoSrc ? protoSrc : srcDoc);
+                                linkDoc.SetData(KeyStore.BrushingDocs, [dstTarg, srcTarg], ListField);
+                                dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) })
+                                srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) })
+                            }))
+                        )
+                    } else {
+                        dstDoc.GetTAsync(KeyStore.Prototype, Document).then((protoDest) =>
+                            srcDoc.GetTAsync(KeyStore.Prototype, Document).then((protoSrc) => runInAction(() => {
+
+                                let dstTarg = (protoDest ? protoDest : dstDoc);
+                                let srcTarg = (protoSrc ? protoSrc : srcDoc);
+                                dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, field => { (field as ListField<Document>).Data.length = 0 })
+                                srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, field => { (field as ListField<Document>).Data.length = 0 })
+                            }))
+                        )
+                    }
+                }
+            }
+        });
+    }
     documentAnchors(view: DocumentView) {
         let equalViews = [view];
         let containerDoc = view.props.Document.GetT(KeyStore.AnnotationOn, Document);
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
index aa0b9ce92..1f039e592 100644
--- a/src/fields/KeyStore.ts
+++ b/src/fields/KeyStore.ts
@@ -29,6 +29,7 @@ export namespace KeyStore {
     export const Caption = new Key("Caption");
     export const ActiveWorkspace = new Key("ActiveWorkspace");
     export const DocumentText = new Key("DocumentText");
+    export const BrushingDocs = new Key("BrushingDocs");
     export const LinkedToDocs = new Key("LinkedToDocs");
     export const LinkedFromDocs = new Key("LinkedFromDocs");
     export const LinkDescription = new Key("LinkDescription");
-- 
cgit v1.2.3-70-g09d2


From 79f9bc805f281a13fe59162f2e25dd1fdcefaae0 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 29 Mar 2019 20:13:06 -0400
Subject: code cleanup

---
 src/client/northstar/dash-fields/HistogramField.ts |  24 +-
 .../dash-nodes/HistogramBinPrimitiveCollection.ts  | 238 ++++++++++++++++++++
 src/client/northstar/dash-nodes/HistogramBox.tsx   |  21 +-
 .../dash-nodes/HistogramBoxPrimitives.scss         |   4 +
 .../dash-nodes/HistogramBoxPrimitives.tsx          | 246 +--------------------
 .../dash-nodes/HistogramLabelPrimitives.tsx        |   3 +-
 .../northstar/operations/HistogramOperation.ts     |  73 +++---
 7 files changed, 301 insertions(+), 308 deletions(-)
 create mode 100644 src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts

(limited to 'src/client/northstar/dash-nodes/HistogramBox.tsx')

diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts
index 1929f8dcd..ae83ea5ba 100644
--- a/src/client/northstar/dash-fields/HistogramField.ts
+++ b/src/client/northstar/dash-fields/HistogramField.ts
@@ -15,9 +15,17 @@ export class HistogramField extends BasicField<HistogramOperation> {
         super(data ? data : HistogramOperation.Empty, save, id);
     }
 
+    omitKeys(obj: any, keys: any) {
+        var dup: any = {};
+        for (var key in obj) {
+            if (keys.indexOf(key) == -1) {
+                dup[key] = obj[key];
+            }
+        }
+        return dup;
+    }
     toString(): string {
-        let omitted = this.omitKeys(this.Data, ['Links', 'BrushLinks']);
-        return JSON.stringify(omitted);
+        return JSON.stringify(this.omitKeys(this.Data, ['Links', 'BrushLinks', 'Result']));
     }
 
     Copy(): Field {
@@ -28,22 +36,12 @@ export class HistogramField extends BasicField<HistogramOperation> {
         return `new HistogramField("${this.Data}")`;
     }
 
-    omitKeys(obj: any, keys: any) {
-        var dup: any = {};
-        for (var key in obj) {
-            if (keys.indexOf(key) == -1) {
-                dup[key] = obj[key];
-            }
-        }
-        return dup;
-    }
 
     ToJson(): { type: Types, data: string, _id: string } {
-        let omitted = this.omitKeys(this.Data, ['Links', 'BrushLinks']);
         return {
             type: Types.HistogramOp,
 
-            data: JSON.stringify(omitted),
+            data: this.toString(),
             _id: this.Id
         }
     }
diff --git a/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
new file mode 100644
index 000000000..43e768c62
--- /dev/null
+++ b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
@@ -0,0 +1,238 @@
+import React = require("react")
+import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel";
+import { ChartType } from '../../northstar/model/binRanges/VisualBinRange';
+import { AggregateFunction, Bin, Brush, DoubleValueAggregateResult, HistogramResult, MarginAggregateParameters, MarginAggregateResult } from "../../northstar/model/idea/idea";
+import { ModelHelpers } from "../../northstar/model/ModelHelpers";
+import { LABColor } from '../../northstar/utils/LABcolor';
+import { PIXIRectangle } from "../../northstar/utils/MathUtil";
+import { StyleConstants } from "../../northstar/utils/StyleContants";
+import { HistogramBox } from "./HistogramBox";
+import "./HistogramBoxPrimitives.scss";
+
+export class HistogramBinPrimitive {
+    constructor(init?: Partial<HistogramBinPrimitive>) {
+        Object.assign(this, init);
+    }
+    public DataValue: number = 0;
+    public Rect: PIXIRectangle = PIXIRectangle.EMPTY;
+    public MarginRect: PIXIRectangle = PIXIRectangle.EMPTY;
+    public MarginPercentage: number = 0;
+    public Color: number = StyleConstants.WARNING_COLOR;
+    public Opacity: number = 1;
+    public BrushIndex: number = 0;
+    public BarAxis: number = -1;
+}
+
+export class HistogramBinPrimitiveCollection {
+    private static TOLERANCE: number = 0.0001;
+
+    private _histoBox: HistogramBox;
+    private get histoOp() { return this._histoBox.HistoOp; }
+    private get histoResult() { return this.histoOp.Result as HistogramResult; }
+    private get sizeConverter() { return this._histoBox.SizeConverter!; }
+    public BinPrimitives: Array<HistogramBinPrimitive> = new Array<HistogramBinPrimitive>();
+    public HitGeom: PIXIRectangle = PIXIRectangle.EMPTY;
+
+    constructor(bin: Bin, histoBox: HistogramBox) {
+        this._histoBox = histoBox;
+        let brushing = this.setupBrushing(bin, this.histoOp.Normalization); // X= 0, Y = 1, V = 2
+
+        brushing.orderedBrushes.reduce((brushFactorSum, brush) => {
+            switch (histoBox.ChartType) {
+                case ChartType.VerticalBar: return this.createVerticalBarChartBinPrimitives(bin, brush, brushing.maxAxis, this.histoOp.Normalization);
+                case ChartType.HorizontalBar: return this.createHorizontalBarChartBinPrimitives(bin, brush, brushing.maxAxis, this.histoOp.Normalization);
+                case ChartType.SinglePoint: return this.createSinglePointChartBinPrimitives(bin, brush);
+                case ChartType.HeatMap: return this.createHeatmapBinPrimitives(bin, brush, brushFactorSum);
+            }
+        }, 0);
+
+        // adjust brush rects (stacking or not)
+        var allBrushIndex = ModelHelpers.AllBrushIndex(this.histoResult);
+        var filteredBinPrims = this.BinPrimitives.filter(b => b.BrushIndex != allBrushIndex && b.DataValue != 0.0);
+        filteredBinPrims.reduce((sum, fbp) => {
+            if (histoBox.ChartType == ChartType.VerticalBar) {
+                if (this.histoOp.Y.AggregateFunction == AggregateFunction.Count) {
+                    fbp.Rect = new PIXIRectangle(fbp.Rect.x, fbp.Rect.y - sum, fbp.Rect.width, fbp.Rect.height);
+                    fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x, fbp.MarginRect.y - sum, fbp.MarginRect.width, fbp.MarginRect.height);
+                    return sum + fbp.Rect.height;
+                }
+                if (this.histoOp.Y.AggregateFunction == AggregateFunction.Avg) {
+                    var w = fbp.Rect.width / 2.0;
+                    fbp.Rect = new PIXIRectangle(fbp.Rect.x + sum, fbp.Rect.y, fbp.Rect.width / filteredBinPrims.length, fbp.Rect.height);
+                    fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x - w + sum + (fbp.Rect.width / 2.0), fbp.MarginRect.y, fbp.MarginRect.width, fbp.MarginRect.height);
+                    return sum + fbp.Rect.width;
+                }
+            }
+            else if (histoBox.ChartType == ChartType.HorizontalBar) {
+                if (this.histoOp.X.AggregateFunction == AggregateFunction.Count) {
+                    fbp.Rect = new PIXIRectangle(fbp.Rect.x + sum, fbp.Rect.y, fbp.Rect.width, fbp.Rect.height);
+                    fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x + sum, fbp.MarginRect.y, fbp.MarginRect.width, fbp.MarginRect.height);
+                    return sum + fbp.Rect.width;
+                }
+                if (this.histoOp.X.AggregateFunction == AggregateFunction.Avg) {
+                    var h = fbp.Rect.height / 2.0;
+                    fbp.Rect = new PIXIRectangle(fbp.Rect.x, fbp.Rect.y + sum, fbp.Rect.width, fbp.Rect.height / filteredBinPrims.length);
+                    fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x, fbp.MarginRect.y - h + sum + (fbp.Rect.height / 2.0), fbp.MarginRect.width, fbp.MarginRect.height);
+                    return sum + fbp.Rect.height;
+                }
+            }
+            return 0;
+        }, 0);
+        this.BinPrimitives = this.BinPrimitives.reverse();
+        var f = this.BinPrimitives.filter(b => b.BrushIndex == allBrushIndex);
+        this.HitGeom = f.length > 0 ? f[0].Rect : PIXIRectangle.EMPTY;
+    }
+
+    private setupBrushing(bin: Bin, normalization: number) {
+        var overlapBrushIndex = ModelHelpers.OverlapBrushIndex(this.histoResult);
+        var orderedBrushes = [this.histoResult.brushes![0], this.histoResult.brushes![overlapBrushIndex]];
+        this.histoResult.brushes!.map(brush => brush.brushIndex != 0 && brush.brushIndex != overlapBrushIndex && orderedBrushes.push(brush));
+        return {
+            orderedBrushes,
+            maxAxis: orderedBrushes.reduce((prev, Brush) => {
+                let aggResult = this.getBinValue(normalization, bin, Brush.brushIndex!);
+                return aggResult != undefined && aggResult > prev ? aggResult : prev;
+            }, Number.MIN_VALUE)
+        };
+    }
+
+    private createHeatmapBinPrimitives(bin: Bin, brush: Brush, brushFactorSum: number): number {
+
+        let unNormalizedValue = this.getBinValue(2, bin, brush.brushIndex!);
+        if (unNormalizedValue == undefined)
+            return brushFactorSum;
+
+        var normalizedValue = (unNormalizedValue - this._histoBox.ValueRange[0]) / (Math.abs((this._histoBox.ValueRange[1] - this._histoBox.ValueRange[0])) < HistogramBinPrimitiveCollection.TOLERANCE ?
+            unNormalizedValue : this._histoBox.ValueRange[1] - this._histoBox.ValueRange[0]);
+
+        let allUnNormalizedValue = this.getBinValue(2, bin, ModelHelpers.AllBrushIndex(this.histoResult))
+
+        // bcz: are these calls needed?  
+        let [xFrom, xTo] = this.sizeConverter.DataToScreenXAxisRange(this._histoBox.VisualBinRanges, 0, bin);
+        let [yFrom, yTo] = this.sizeConverter.DataToScreenYAxisRange(this._histoBox.VisualBinRanges, 1, bin);
+
+        var returnBrushFactorSum = brushFactorSum;
+        if (allUnNormalizedValue != undefined) {
+            var brushFactor = (unNormalizedValue / allUnNormalizedValue);
+            returnBrushFactorSum += brushFactor;
+            returnBrushFactorSum = Math.min(returnBrushFactorSum, 1.0);
+
+            var tempRect = new PIXIRectangle(xFrom, yTo, xTo - xFrom, yFrom - yTo);
+            var ratio = (tempRect.width / tempRect.height);
+            var newHeight = Math.sqrt((1.0 / ratio) * ((tempRect.width * tempRect.height) * returnBrushFactorSum));
+            var newWidth = newHeight * ratio;
+
+            xFrom = (tempRect.x + (tempRect.width - newWidth) / 2.0);
+            yTo = (tempRect.y + (tempRect.height - newHeight) / 2.0);
+            xTo = (xFrom + newWidth);
+            yFrom = (yTo + newHeight);
+        }
+        var alpha = 0.0;
+        var color = this.baseColorFromBrush(brush);
+        var lerpColor = LABColor.Lerp(
+            LABColor.FromColor(StyleConstants.MIN_VALUE_COLOR),
+            LABColor.FromColor(color),
+            (alpha + Math.pow(normalizedValue, 1.0 / 3.0) * (1.0 - alpha)));
+        var dataColor = LABColor.ToColor(lerpColor);
+
+        this.createBinPrimitive(-1, brush, PIXIRectangle.EMPTY, 0, xFrom, xTo, yFrom, yTo, dataColor, 1, unNormalizedValue);
+        return returnBrushFactorSum;
+    }
+
+    private createSinglePointChartBinPrimitives(bin: Bin, brush: Brush): number {
+        let unNormalizedValue = this.getBinValue(2, bin, brush.brushIndex!);
+        if (unNormalizedValue != undefined) {
+            let [xFrom, xTo] = this.sizeConverter.DataToScreenPointRange(0, bin, ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, this.histoOp.X, this.histoResult, brush.brushIndex!));
+            let [yFrom, yTo] = this.sizeConverter.DataToScreenPointRange(1, bin, ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, this.histoOp.Y, this.histoResult, brush.brushIndex!));
+
+            if (xFrom != undefined && yFrom != undefined && xTo != undefined && yTo != undefined)
+                this.createBinPrimitive(-1, brush, PIXIRectangle.EMPTY, 0, xFrom, xTo, yFrom, yTo, this.baseColorFromBrush(brush), 1, unNormalizedValue);
+        }
+        return 0;
+    }
+
+    private createVerticalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number): number {
+        let dataValue = this.getBinValue(1, bin, brush.brushIndex!);
+        if (dataValue != undefined) {
+            let [yFrom, yValue, yTo] = this.sizeConverter.DataToScreenNormalizedRange(dataValue, normalization, 1, binBrushMaxAxis);
+            let [xFrom, xTo] = this.sizeConverter.DataToScreenXAxisRange(this._histoBox.VisualBinRanges, 0, bin);
+
+            var yMarginAbsolute = this.getMargin(bin, brush, this.histoOp.Y);
+            var marginRect = new PIXIRectangle(xFrom + (xTo - xFrom) / 2.0 - 1,
+                this.sizeConverter.DataToScreenY(yValue + yMarginAbsolute), 2,
+                this.sizeConverter.DataToScreenY(yValue - yMarginAbsolute) - this.sizeConverter.DataToScreenY(yValue + yMarginAbsolute));
+
+            this.createBinPrimitive(1, brush, marginRect, 0, xFrom, xTo, yFrom, yTo,
+                this.baseColorFromBrush(brush), normalization != 0 ? 1 : 0.6 * binBrushMaxAxis / this.sizeConverter.DataRanges[1] + 0.4, dataValue);
+        }
+        return 0;
+    }
+
+    private createHorizontalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number): number {
+        let dataValue = this.getBinValue(0, bin, brush.brushIndex!);
+        if (dataValue != undefined) {
+            let [xFrom, xValue, xTo] = this.sizeConverter.DataToScreenNormalizedRange(dataValue, normalization, 0, binBrushMaxAxis);
+            let [yFrom, yTo] = this.sizeConverter.DataToScreenYAxisRange(this._histoBox.VisualBinRanges, 1, bin);
+
+            var xMarginAbsolute = this.sizeConverter.IsSmall ? 0 : this.getMargin(bin, brush, this.histoOp.X);
+            var marginRect = new PIXIRectangle(this.sizeConverter.DataToScreenX(xValue - xMarginAbsolute),
+                yTo + (yFrom - yTo) / 2.0 - 1,
+                this.sizeConverter.DataToScreenX(xValue + xMarginAbsolute) - this.sizeConverter.DataToScreenX(xValue - xMarginAbsolute),
+                2.0);
+
+            this.createBinPrimitive(0, brush, marginRect, 0, xFrom, xTo, yFrom, yTo,
+                this.baseColorFromBrush(brush), normalization != 1 ? 1 : 0.6 * binBrushMaxAxis / this.sizeConverter.DataRanges[0] + 0.4, dataValue);
+        }
+        return 0;
+    }
+
+    public getBinValue(axis: number, bin: Bin, brushIndex: number) {
+        var aggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, axis == 0 ? this.histoOp.X : axis == 1 ? this.histoOp.Y : this.histoOp.V, this.histoResult, brushIndex);
+        let dataValue = ModelHelpers.GetAggregateResult(bin, aggregateKey) as DoubleValueAggregateResult;
+        return dataValue != null && dataValue.hasResult ? dataValue.result : undefined;
+    }
+
+    private getMargin(bin: Bin, brush: Brush, axis: AttributeTransformationModel) {
+        var marginParams = new MarginAggregateParameters();
+        marginParams.aggregateFunction = axis.AggregateFunction;
+        var marginAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, axis, this.histoResult, brush.brushIndex!, marginParams);
+        var marginResult = ModelHelpers.GetAggregateResult(bin, marginAggregateKey) as MarginAggregateResult;
+        return !marginResult ? 0 : marginResult.absolutMargin!;
+    }
+
+    private createBinPrimitive(barAxis: number, brush: Brush, marginRect: PIXIRectangle,
+        marginPercentage: number, xFrom: number, xTo: number, yFrom: number, yTo: number, color: number, opacity: number, dataValue: number) {
+        var binPrimitive = new HistogramBinPrimitive(
+            {
+                Rect: new PIXIRectangle(xFrom, yTo, xTo - xFrom, yFrom - yTo),
+                MarginRect: marginRect,
+                MarginPercentage: marginPercentage,
+                BrushIndex: brush.brushIndex,
+                Color: color,
+                Opacity: opacity,
+                DataValue: dataValue,
+                BarAxis: barAxis
+            });
+        this.BinPrimitives.push(binPrimitive);
+    }
+
+    private baseColorFromBrush(brush: Brush): number {
+        let bc = StyleConstants.BRUSH_COLORS;
+        if (brush.brushIndex == ModelHelpers.RestBrushIndex(this.histoResult)) {
+            return StyleConstants.HIGHLIGHT_COLOR;
+        }
+        else if (brush.brushIndex == ModelHelpers.OverlapBrushIndex(this.histoResult)) {
+            return StyleConstants.OVERLAP_COLOR;
+        }
+        else if (brush.brushIndex == ModelHelpers.AllBrushIndex(this.histoResult)) {
+            return 0x00ff00;
+        }
+        else if (bc.length > 0) {
+            return bc[brush.brushIndex! % bc.length];
+        }
+        // else if (this.histoOp.BrushColors.length > 0) {
+        //     return this.histoOp.BrushColors[brush.brushIndex! % this.histoOp.BrushColors.length];
+        // }
+        return StyleConstants.HIGHLIGHT_COLOR;
+    }
+}
\ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index 9976ff6ad..6bf2697fc 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -22,9 +22,6 @@ import { HistogramBoxPrimitives } from './HistogramBoxPrimitives';
 import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives";
 import { StyleConstants } from "../utils/StyleContants";
 
-export interface HistogramPrimitivesProps {
-    HistoBox: HistogramBox;
-}
 
 @observer
 export class HistogramBox extends React.Component<FieldViewProps> {
@@ -77,6 +74,15 @@ export class HistogramBox extends React.Component<FieldViewProps> {
         }
     }
 
+    @action
+    xLabelPointerDown = (e: React.PointerEvent) => {
+        this.HistoOp.X = new AttributeTransformationModel(this.HistoOp.X.AttributeModel, this.HistoOp.X.AggregateFunction == AggregateFunction.None ? AggregateFunction.Count : AggregateFunction.None);
+    }
+    @action
+    yLabelPointerDown = (e: React.PointerEvent) => {
+        this.HistoOp.Y = new AttributeTransformationModel(this.HistoOp.Y.AttributeModel, this.HistoOp.Y.AggregateFunction == AggregateFunction.None ? AggregateFunction.Count : AggregateFunction.None);
+    }
+
     componentDidMount() {
         if (this._dropXRef.current) {
             this._dropXDisposer = DragManager.MakeDropTarget(this._dropXRef.current, { handlers: { drop: this.dropX.bind(this) } });
@@ -102,15 +108,6 @@ export class HistogramBox extends React.Component<FieldViewProps> {
             });
     }
 
-    @action
-    xLabelPointerDown = (e: React.PointerEvent) => {
-        this.HistoOp.X = new AttributeTransformationModel(this.HistoOp.X.AttributeModel, this.HistoOp.X.AggregateFunction == AggregateFunction.None ? AggregateFunction.Count : AggregateFunction.None);
-    }
-    @action
-    yLabelPointerDown = (e: React.PointerEvent) => {
-        this.HistoOp.Y = new AttributeTransformationModel(this.HistoOp.Y.AttributeModel, this.HistoOp.Y.AggregateFunction == AggregateFunction.None ? AggregateFunction.Count : AggregateFunction.None);
-    }
-
     componentWillUnmount() {
         if (this._dropXDisposer)
             this._dropXDisposer();
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss
index 9d42219cc..ce9edd65e 100644
--- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss
@@ -1,3 +1,7 @@
+.histogramboxprimitives-container {
+    width: 100%;
+    height: 100%;
+}
 .histogramboxprimitives-border {
     border: 3px;
     border-style: solid;
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
index c97acb064..e9adb3ce5 100644
--- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
@@ -1,27 +1,24 @@
 import React = require("react")
-import { computed, observable, runInAction, reaction, untracked, trace } from "mobx";
+import { computed, observable, reaction, runInAction, trace } from "mobx";
 import { observer } from "mobx-react";
 import { Utils as DashUtils } from '../../../Utils';
-import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel";
 import { FilterModel } from "../../northstar/core/filter/FilterModel";
-import { ChartType } from '../../northstar/model/binRanges/VisualBinRange';
-import { AggregateFunction, Bin, Brush, HistogramResult, MarginAggregateParameters, MarginAggregateResult } from "../../northstar/model/idea/idea";
 import { ModelHelpers } from "../../northstar/model/ModelHelpers";
 import { ArrayUtil } from "../../northstar/utils/ArrayUtil";
 import { LABColor } from '../../northstar/utils/LABcolor';
 import { PIXIRectangle } from "../../northstar/utils/MathUtil";
 import { StyleConstants } from "../../northstar/utils/StyleContants";
-import { HistogramBox, HistogramPrimitivesProps } from "./HistogramBox";
+import { HistogramBinPrimitiveCollection, HistogramBinPrimitive } from "./HistogramBinPrimitiveCollection";
+import { HistogramBox } from "./HistogramBox";
 import "./HistogramBoxPrimitives.scss";
 
-
+export interface HistogramPrimitivesProps {
+    HistoBox: HistogramBox;
+}
 @observer
 export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesProps> {
     private get histoOp() { return this.props.HistoBox.HistoOp; }
     private get renderDimension() { return this.props.HistoBox.SizeConverter.RenderDimension; }
-    componentDidMount() {
-        reaction(() => this.props.HistoBox.HistoOp.FilterString, () => this._selectedPrims.length = this.histoOp.FilterModels.length = 0);
-    }
     @observable _selectedPrims: HistogramBinPrimitive[] = [];
     @computed get xaxislines() { return this.renderGridLinesAndLabels(0); }
     @computed get yaxislines() { return this.renderGridLinesAndLabels(1); }
@@ -42,6 +39,10 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
         }, [] as JSX.Element[]);
     }
 
+    componentDidMount() {
+        reaction(() => this.props.HistoBox.HistoOp.FilterString, () => this._selectedPrims.length = this.histoOp.FilterModels.length = 0);
+    }
+
     private getSelectionToggle(binPrimitives: HistogramBinPrimitive[], allBrushIndex: number, filterModel: FilterModel) {
         let allBrushPrim = ArrayUtil.FirstOrDefault(binPrimitives, bp => bp.BrushIndex == allBrushIndex);
         return !allBrushPrim ? () => { } : () => runInAction(() => {
@@ -90,7 +91,6 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
         let line = (<div className="histogramboxprimitives-line" style={{ width: trans2Xpercent, height: trans2Ypercent, }} />);
         return this.drawEntity(xFrom, yFrom, line);
     }
-
     drawRect(r: PIXIRectangle, barAxis: number, color: number | undefined, classExt: string, tapHandler: () => void = () => { }) {
         if (r.height < 0) {
             r.y += r.height;
@@ -114,235 +114,11 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
         return this.drawEntity(r.x, r.y, rect);
     }
     render() {
-        return <div className="histogramboxprimitives-container" style={{
-            width: "100%",
-            height: "100%",
-        }}>
+        return <div className="histogramboxprimitives-container">
             {this.xaxislines}
             {this.yaxislines}
             {this.binPrimitives}
             {this.selectedPrimitives}
         </div>
     }
-}
-
-class HistogramBinPrimitive {
-    constructor(init?: Partial<HistogramBinPrimitive>) {
-        Object.assign(this, init);
-    }
-    public DataValue: number = 0;
-    public Rect: PIXIRectangle = PIXIRectangle.EMPTY;
-    public MarginRect: PIXIRectangle = PIXIRectangle.EMPTY;
-    public MarginPercentage: number = 0;
-    public Color: number = StyleConstants.WARNING_COLOR;
-    public Opacity: number = 1;
-    public BrushIndex: number = 0;
-    public BarAxis: number = -1;
-}
-
-export class HistogramBinPrimitiveCollection {
-    private static TOLERANCE: number = 0.0001;
-
-    private _histoBox: HistogramBox;
-    private get histoOp() { return this._histoBox.HistoOp; }
-    private get histoResult() { return this.histoOp.Result as HistogramResult; }
-    private get sizeConverter() { return this._histoBox.SizeConverter!; }
-    public BinPrimitives: Array<HistogramBinPrimitive> = new Array<HistogramBinPrimitive>();
-    public HitGeom: PIXIRectangle = PIXIRectangle.EMPTY;
-
-    constructor(bin: Bin, histoBox: HistogramBox) {
-        this._histoBox = histoBox;
-        let brushing = this.setupBrushing(bin, this.histoOp.Normalization); // X= 0, Y = 1, V = 2
-
-        brushing.orderedBrushes.reduce((brushFactorSum, brush) => {
-            switch (histoBox.ChartType) {
-                case ChartType.VerticalBar: return this.createVerticalBarChartBinPrimitives(bin, brush, brushing.maxAxis, this.histoOp.Normalization);
-                case ChartType.HorizontalBar: return this.createHorizontalBarChartBinPrimitives(bin, brush, brushing.maxAxis, this.histoOp.Normalization);
-                case ChartType.SinglePoint: return this.createSinglePointChartBinPrimitives(bin, brush);
-                case ChartType.HeatMap: return this.createHeatmapBinPrimitives(bin, brush, brushFactorSum);
-            }
-        }, 0);
-
-        // adjust brush rects (stacking or not)
-        var allBrushIndex = ModelHelpers.AllBrushIndex(this.histoResult);
-        var filteredBinPrims = this.BinPrimitives.filter(b => b.BrushIndex != allBrushIndex && b.DataValue != 0.0);
-        filteredBinPrims.reduce((sum, fbp) => {
-            if (histoBox.ChartType == ChartType.VerticalBar) {
-                if (this.histoOp.Y.AggregateFunction == AggregateFunction.Count) {
-                    fbp.Rect = new PIXIRectangle(fbp.Rect.x, fbp.Rect.y - sum, fbp.Rect.width, fbp.Rect.height);
-                    fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x, fbp.MarginRect.y - sum, fbp.MarginRect.width, fbp.MarginRect.height);
-                    return sum + fbp.Rect.height;
-                }
-                if (this.histoOp.Y.AggregateFunction == AggregateFunction.Avg) {
-                    var w = fbp.Rect.width / 2.0;
-                    fbp.Rect = new PIXIRectangle(fbp.Rect.x + sum, fbp.Rect.y, fbp.Rect.width / filteredBinPrims.length, fbp.Rect.height);
-                    fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x - w + sum + (fbp.Rect.width / 2.0), fbp.MarginRect.y, fbp.MarginRect.width, fbp.MarginRect.height);
-                    return sum + fbp.Rect.width;
-                }
-            }
-            else if (histoBox.ChartType == ChartType.HorizontalBar) {
-                if (this.histoOp.X.AggregateFunction == AggregateFunction.Count) {
-                    fbp.Rect = new PIXIRectangle(fbp.Rect.x + sum, fbp.Rect.y, fbp.Rect.width, fbp.Rect.height);
-                    fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x + sum, fbp.MarginRect.y, fbp.MarginRect.width, fbp.MarginRect.height);
-                    return sum + fbp.Rect.width;
-                }
-                if (this.histoOp.X.AggregateFunction == AggregateFunction.Avg) {
-                    var h = fbp.Rect.height / 2.0;
-                    fbp.Rect = new PIXIRectangle(fbp.Rect.x, fbp.Rect.y + sum, fbp.Rect.width, fbp.Rect.height / filteredBinPrims.length);
-                    fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x, fbp.MarginRect.y - h + sum + (fbp.Rect.height / 2.0), fbp.MarginRect.width, fbp.MarginRect.height);
-                    return sum + fbp.Rect.height;
-                }
-            }
-            return 0;
-        }, 0);
-        this.BinPrimitives = this.BinPrimitives.reverse();
-        var f = this.BinPrimitives.filter(b => b.BrushIndex == allBrushIndex);
-        this.HitGeom = f.length > 0 ? f[0].Rect : PIXIRectangle.EMPTY;
-    }
-    private setupBrushing(bin: Bin, normalization: number) {
-        var overlapBrushIndex = ModelHelpers.OverlapBrushIndex(this.histoResult);
-        var orderedBrushes = [this.histoResult.brushes![0], this.histoResult.brushes![overlapBrushIndex]];
-        this.histoResult.brushes!.map(brush => brush.brushIndex != 0 && brush.brushIndex != overlapBrushIndex && orderedBrushes.push(brush));
-        return {
-            orderedBrushes,
-            maxAxis: orderedBrushes.reduce((prev, Brush) => {
-                let aggResult = this.histoOp.getValue(normalization, bin, this.histoResult, Brush.brushIndex!);
-                return aggResult != undefined && aggResult > prev ? aggResult : prev;
-            }, Number.MIN_VALUE)
-        };
-    }
-    private createHeatmapBinPrimitives(bin: Bin, brush: Brush, brushFactorSum: number): number {
-
-        let unNormalizedValue = this.histoOp.getValue(2, bin, this.histoResult, brush.brushIndex!);
-        if (unNormalizedValue == undefined)
-            return brushFactorSum;
-
-        var normalizedValue = (unNormalizedValue - this._histoBox.ValueRange[0]) / (Math.abs((this._histoBox.ValueRange[1] - this._histoBox.ValueRange[0])) < HistogramBinPrimitiveCollection.TOLERANCE ?
-            unNormalizedValue : this._histoBox.ValueRange[1] - this._histoBox.ValueRange[0]);
-
-        let allUnNormalizedValue = this.histoOp.getValue(2, bin, this.histoResult, ModelHelpers.AllBrushIndex(this.histoResult))
-
-        // bcz: are these calls needed?  
-        let [xFrom, xTo] = this.sizeConverter.DataToScreenXAxisRange(this._histoBox.VisualBinRanges, 0, bin);
-        let [yFrom, yTo] = this.sizeConverter.DataToScreenYAxisRange(this._histoBox.VisualBinRanges, 1, bin);
-
-        var returnBrushFactorSum = brushFactorSum;
-        if (allUnNormalizedValue != undefined) {
-            var brushFactor = (unNormalizedValue / allUnNormalizedValue);
-            returnBrushFactorSum += brushFactor;
-            returnBrushFactorSum = Math.min(returnBrushFactorSum, 1.0);
-
-            var tempRect = new PIXIRectangle(xFrom, yTo, xTo - xFrom, yFrom - yTo);
-            var ratio = (tempRect.width / tempRect.height);
-            var newHeight = Math.sqrt((1.0 / ratio) * ((tempRect.width * tempRect.height) * returnBrushFactorSum));
-            var newWidth = newHeight * ratio;
-
-            xFrom = (tempRect.x + (tempRect.width - newWidth) / 2.0);
-            yTo = (tempRect.y + (tempRect.height - newHeight) / 2.0);
-            xTo = (xFrom + newWidth);
-            yFrom = (yTo + newHeight);
-        }
-        var alpha = 0.0;
-        var color = this.baseColorFromBrush(brush);
-        var lerpColor = LABColor.Lerp(
-            LABColor.FromColor(StyleConstants.MIN_VALUE_COLOR),
-            LABColor.FromColor(color),
-            (alpha + Math.pow(normalizedValue, 1.0 / 3.0) * (1.0 - alpha)));
-        var dataColor = LABColor.ToColor(lerpColor);
-
-        this.createBinPrimitive(-1, brush, PIXIRectangle.EMPTY, 0, xFrom, xTo, yFrom, yTo, dataColor, 1, unNormalizedValue);
-        return returnBrushFactorSum;
-    }
-
-    private createSinglePointChartBinPrimitives(bin: Bin, brush: Brush): number {
-        let unNormalizedValue = this._histoBox.HistoOp.getValue(2, bin, this.histoResult, brush.brushIndex!);
-        if (unNormalizedValue != undefined) {
-            let [xFrom, xTo] = this.sizeConverter.DataToScreenPointRange(0, bin, ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, this.histoOp.X, this.histoResult, brush.brushIndex!));
-            let [yFrom, yTo] = this.sizeConverter.DataToScreenPointRange(1, bin, ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, this.histoOp.Y, this.histoResult, brush.brushIndex!));
-
-            if (xFrom != undefined && yFrom != undefined && xTo != undefined && yTo != undefined)
-                this.createBinPrimitive(-1, brush, PIXIRectangle.EMPTY, 0, xFrom, xTo, yFrom, yTo, this.baseColorFromBrush(brush), 1, unNormalizedValue);
-        }
-        return 0;
-    }
-
-    private createVerticalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number): number {
-        let dataValue = this.histoOp.getValue(1, bin, this.histoResult, brush.brushIndex!);
-        if (dataValue != undefined) {
-            let [yFrom, yValue, yTo] = this.sizeConverter.DataToScreenNormalizedRange(dataValue, normalization, 1, binBrushMaxAxis);
-            let [xFrom, xTo] = this.sizeConverter.DataToScreenXAxisRange(this._histoBox.VisualBinRanges, 0, bin);
-
-            var yMarginAbsolute = this.getMargin(bin, brush, this.histoOp.Y);
-            var marginRect = new PIXIRectangle(xFrom + (xTo - xFrom) / 2.0 - 1,
-                this.sizeConverter.DataToScreenY(yValue + yMarginAbsolute), 2,
-                this.sizeConverter.DataToScreenY(yValue - yMarginAbsolute) - this.sizeConverter.DataToScreenY(yValue + yMarginAbsolute));
-
-            this.createBinPrimitive(1, brush, marginRect, 0, xFrom, xTo, yFrom, yTo,
-                this.baseColorFromBrush(brush), normalization != 0 ? 1 : 0.6 * binBrushMaxAxis / this.sizeConverter.DataRanges[1] + 0.4, dataValue);
-        }
-        return 0;
-    }
-
-    private createHorizontalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number): number {
-        let dataValue = this.histoOp.getValue(0, bin, this.histoResult, brush.brushIndex!);
-        if (dataValue != undefined) {
-            let [xFrom, xValue, xTo] = this.sizeConverter.DataToScreenNormalizedRange(dataValue, normalization, 0, binBrushMaxAxis);
-            let [yFrom, yTo] = this.sizeConverter.DataToScreenYAxisRange(this._histoBox.VisualBinRanges, 1, bin);
-
-            var xMarginAbsolute = this.sizeConverter.IsSmall ? 0 : this.getMargin(bin, brush, this.histoOp.X);
-            var marginRect = new PIXIRectangle(this.sizeConverter.DataToScreenX(xValue - xMarginAbsolute),
-                yTo + (yFrom - yTo) / 2.0 - 1,
-                this.sizeConverter.DataToScreenX(xValue + xMarginAbsolute) - this.sizeConverter.DataToScreenX(xValue - xMarginAbsolute),
-                2.0);
-
-            this.createBinPrimitive(0, brush, marginRect, 0, xFrom, xTo, yFrom, yTo,
-                this.baseColorFromBrush(brush), normalization != 1 ? 1 : 0.6 * binBrushMaxAxis / this.sizeConverter.DataRanges[0] + 0.4, dataValue);
-        }
-        return 0;
-    }
-
-
-    private getMargin(bin: Bin, brush: Brush, axis: AttributeTransformationModel) {
-        var marginParams = new MarginAggregateParameters();
-        marginParams.aggregateFunction = axis.AggregateFunction;
-        var marginAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, axis, this.histoResult, brush.brushIndex!, marginParams);
-        var marginResult = ModelHelpers.GetAggregateResult(bin, marginAggregateKey) as MarginAggregateResult;
-        return !marginResult ? 0 : marginResult.absolutMargin!;
-    }
-
-    private createBinPrimitive(barAxis: number, brush: Brush, marginRect: PIXIRectangle,
-        marginPercentage: number, xFrom: number, xTo: number, yFrom: number, yTo: number, color: number, opacity: number, dataValue: number) {
-        var binPrimitive = new HistogramBinPrimitive(
-            {
-                Rect: new PIXIRectangle(xFrom, yTo, xTo - xFrom, yFrom - yTo),
-                MarginRect: marginRect,
-                MarginPercentage: marginPercentage,
-                BrushIndex: brush.brushIndex,
-                Color: color,
-                Opacity: opacity,
-                DataValue: dataValue,
-                BarAxis: barAxis
-            });
-        this.BinPrimitives.push(binPrimitive);
-    }
-
-    private baseColorFromBrush(brush: Brush): number {
-        let bc = StyleConstants.BRUSH_COLORS;
-        if (brush.brushIndex == ModelHelpers.RestBrushIndex(this.histoResult)) {
-            return StyleConstants.HIGHLIGHT_COLOR;
-        }
-        else if (brush.brushIndex == ModelHelpers.OverlapBrushIndex(this.histoResult)) {
-            return StyleConstants.OVERLAP_COLOR;
-        }
-        else if (brush.brushIndex == ModelHelpers.AllBrushIndex(this.histoResult)) {
-            return 0x00ff00;
-        }
-        else if (bc.length > 0) {
-            return bc[brush.brushIndex! % bc.length];
-        }
-        // else if (this.histoOp.BrushColors.length > 0) {
-        //     return this.histoOp.BrushColors[brush.brushIndex! % this.histoOp.BrushColors.length];
-        // }
-        return StyleConstants.HIGHLIGHT_COLOR;
-    }
 }
\ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx
index 45b23874d..93b237deb 100644
--- a/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx
+++ b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx
@@ -5,8 +5,9 @@ import { Utils as DashUtils } from '../../../Utils';
 import { NominalVisualBinRange } from "../model/binRanges/NominalVisualBinRange";
 import "../utils/Extensions";
 import { StyleConstants } from "../utils/StyleContants";
-import { HistogramBox, HistogramPrimitivesProps } from "./HistogramBox";
+import { HistogramBox } from "./HistogramBox";
 import "./HistogramLabelPrimitives.scss";
+import { HistogramPrimitivesProps } from "./HistogramBoxPrimitives";
 
 @observer
 export class HistogramLabelPrimitives extends React.Component<HistogramPrimitivesProps> {
diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
index 48bad73df..4689cb233 100644
--- a/src/client/northstar/operations/HistogramOperation.ts
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -1,5 +1,7 @@
 import { action, computed, observable, trace } from "mobx";
 import { Document } from "../../../fields/Document";
+import { FieldWaiting } from "../../../fields/Field";
+import { KeyStore } from "../../../fields/KeyStore";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 import { ColumnAttributeModel } from "../core/attribute/AttributeModel";
 import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
@@ -7,52 +9,30 @@ import { CalculatedAttributeManager } from "../core/attribute/CalculatedAttribut
 import { FilterModel } from "../core/filter/FilterModel";
 import { FilterOperand } from "../core/filter/FilterOperand";
 import { IBaseFilterConsumer } from "../core/filter/IBaseFilterConsumer";
-import { IBaseFilterProvider, instanceOfIBaseFilterProvider } from "../core/filter/IBaseFilterProvider";
+import { IBaseFilterProvider } from "../core/filter/IBaseFilterProvider";
+import { HistogramField } from "../dash-fields/HistogramField";
 import { SETTINGS_SAMPLE_SIZE, SETTINGS_X_BINS, SETTINGS_Y_BINS } from "../model/binRanges/VisualBinRangeHelper";
-import { AggregateFunction, AggregateParameters, Attribute, AverageAggregateParameters, DataType, HistogramOperationParameters, QuantitativeBinRange, HistogramResult, Brush, DoubleValueAggregateResult, Bin } from "../model/idea/idea";
+import { AggregateFunction, AggregateParameters, Attribute, AverageAggregateParameters, Bin, DataType, DoubleValueAggregateResult, HistogramOperationParameters, HistogramResult, QuantitativeBinRange } from "../model/idea/idea";
 import { ModelHelpers } from "../model/ModelHelpers";
 import { ArrayUtil } from "../utils/ArrayUtil";
 import { BaseOperation } from "./BaseOperation";
-import { KeyStore } from "../../../fields/KeyStore";
-import { HistogramField } from "../dash-fields/HistogramField";
-import { FieldWaiting } from "../../../fields/Field";
-import { StyleConstants } from "../utils/StyleContants";
-
 
 export class HistogramOperation extends BaseOperation implements IBaseFilterConsumer, IBaseFilterProvider {
+    public static Empty = new HistogramOperation("-empty schema-", new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())));
     @observable public FilterOperand: FilterOperand = FilterOperand.AND;
     @observable public Links: Document[] = [];
     @observable public BrushLinks: { l: Document, b: Document }[] = [];
     @observable public BrushColors: number[] = [];
-    @observable public Normalization: number = -1;
     @observable public FilterModels: FilterModel[] = [];
+
+    @observable public Normalization: number = -1;
     @observable public X: AttributeTransformationModel;
     @observable public Y: AttributeTransformationModel;
     @observable public V: AttributeTransformationModel;
     @observable public SchemaName: string;
+    @observable public QRange: QuantitativeBinRange | undefined;
     @computed public get Schema() { return CurrentUserUtils.GetNorthstarSchema(this.SchemaName); }
 
-    @action
-    public AddFilterModels(filterModels: FilterModel[]): void {
-        filterModels.filter(f => f !== null).forEach(fm => this.FilterModels.push(fm));
-    }
-    @action
-    public RemoveFilterModels(filterModels: FilterModel[]): void {
-        ArrayUtil.RemoveMany(this.FilterModels, filterModels);
-    }
-
-    public getValue(axis: number, bin: Bin, result: HistogramResult, brushIndex: number) {
-        var aggregateKey = ModelHelpers.CreateAggregateKey(this.Schema!.distinctAttributeParameters, axis == 0 ? this.X : axis == 1 ? this.Y : this.V, result, brushIndex);
-        let dataValue = ModelHelpers.GetAggregateResult(bin, aggregateKey) as DoubleValueAggregateResult;
-        return dataValue != null && dataValue.hasResult ? dataValue.result : undefined;
-    }
-
-    public static Empty = new HistogramOperation("-empty schema-", new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())));
-
-    Equals(other: Object): boolean {
-        throw new Error("Method not implemented.");
-    }
-
     constructor(schemaName: string, x: AttributeTransformationModel, y: AttributeTransformationModel, v: AttributeTransformationModel, normalized?: number) {
         super();
         this.X = x;
@@ -62,6 +42,19 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
         this.SchemaName = schemaName;
     }
 
+    Equals(other: Object): boolean {
+        throw new Error("Method not implemented.");
+    }
+
+    @action
+    public AddFilterModels(filterModels: FilterModel[]): void {
+        filterModels.filter(f => f !== null).forEach(fm => this.FilterModels.push(fm));
+    }
+    @action
+    public RemoveFilterModels(filterModels: FilterModel[]): void {
+        ArrayUtil.RemoveMany(this.FilterModels, filterModels);
+    }
+
     @computed
     public get FilterString(): string {
         let filterModels: FilterModel[] = [];
@@ -73,25 +66,16 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
         trace();
         let brushes: string[] = [];
         this.BrushLinks.map(brushLink => {
-            let brusherDoc = brushLink.b;
-            let brushHistogram = brusherDoc.GetT(KeyStore.Data, HistogramField);
+            let brushHistogram = brushLink.b.GetT(KeyStore.Data, HistogramField);
             if (brushHistogram && brushHistogram != FieldWaiting) {
                 let filterModels: FilterModel[] = [];
-                let brush = FilterModel.GetFilterModelsRecursive(brushHistogram!.Data, new Set<IBaseFilterProvider>(), filterModels, false)
-                brushes.push(brush);
+                brushes.push(FilterModel.GetFilterModelsRecursive(brushHistogram.Data, new Set<IBaseFilterProvider>(), filterModels, false));
             }
         });
         return brushes;
     }
 
-
-    @computed.struct
-    public get SelectionString() {
-        let filterModels = new Array<FilterModel>();
-        return FilterModel.GetFilterModelsRecursive(this, new Set<IBaseFilterProvider>(), filterModels, false);
-    }
-
-    GetAggregateParameters(histoX: AttributeTransformationModel, histoY: AttributeTransformationModel, histoValue: AttributeTransformationModel) {
+    private 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));
 
@@ -108,11 +92,9 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
         return [perBinAggregateParameters, globalAggregateParameters];
     }
 
-    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);
+            let [perBinAggregateParameters, globalAggregateParameters] = this.getAggregateParameters(this.X, this.Y, this.V);
             return new HistogramOperationParameters({
                 enableBrushComputation: true,
                 adapterName: this.SchemaName,
@@ -131,9 +113,6 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
             });
         }
     }
-    get_random_color(): number {
-        return (Math.floor(Math.random() * 256) << 16) + (Math.floor(Math.random() * 256) << 8) + (Math.floor(Math.random() * 256));
-    }
 
     @action
     public async Update(): Promise<void> {
-- 
cgit v1.2.3-70-g09d2


From a2e4321eb23bedda9b78f012d3eb061045c5b33c Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 29 Mar 2019 22:50:49 -0400
Subject: improved brushing

---
 src/client/northstar/dash-nodes/HistogramBox.tsx   |  5 +-
 src/client/northstar/operations/BaseOperation.ts   |  5 +-
 .../CollectionFreeFormLinksView.tsx                | 56 ++++++++++------------
 3 files changed, 29 insertions(+), 37 deletions(-)

(limited to 'src/client/northstar/dash-nodes/HistogramBox.tsx')

diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index 6bf2697fc..65d917fd3 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -36,7 +36,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {
     @observable public HistoOp: HistogramOperation = HistogramOperation.Empty;
     @observable public VisualBinRanges: VisualBinRange[] = [];
     @observable public ValueRange: number[] = [];
-    @observable public HistogramResult: HistogramResult = new HistogramResult();
+    @computed public get HistogramResult(): HistogramResult { return this.HistoOp.Result as HistogramResult; }
     @observable public SizeConverter: SizeConverter = new SizeConverter();
 
     @computed get createOperationParamsCache() { trace(); return this.HistoOp.CreateOperationParameters(); }
@@ -120,7 +120,6 @@ export class HistogramBox extends React.Component<FieldViewProps> {
             this.props.doc.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt<HistogramField>) => runInAction(() => {
                 this.HistoOp = histoOp ? histoOp.Data : HistogramOperation.Empty;
                 if (this.HistoOp != HistogramOperation.Empty) {
-                    this.HistoOp.YieldResult = (r: Result) => action(() => this.HistogramResult = r as HistogramResult)();
                     reaction(() => this.props.doc.GetList(KeyStore.LinkedFromDocs, []), (docs: Document[]) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
                     reaction(() => this.props.doc.GetList(KeyStore.BrushingDocs, []).length,
                         () => {
@@ -128,7 +127,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {
                             var availableColors = StyleConstants.BRUSH_COLORS.map(c => c);
                             let proto = this.props.doc.GetPrototype() as Document;
                             let brushingLinks = brushingDocs.map((brush, i) => {
-                                // brush.SetNumber(KeyStore.BackgroundColor, availableColors[i % availableColors.length]);
+                                brush.SetNumber(KeyStore.BackgroundColor, availableColors[i % availableColors.length]);
                                 let brushed = brush.GetList(KeyStore.BrushingDocs, [] as Document[]);
                                 if (!brushed || brushed.length < 2)
                                     return undefined;
diff --git a/src/client/northstar/operations/BaseOperation.ts b/src/client/northstar/operations/BaseOperation.ts
index 21d1db749..f545b2c58 100644
--- a/src/client/northstar/operations/BaseOperation.ts
+++ b/src/client/northstar/operations/BaseOperation.ts
@@ -11,7 +11,7 @@ export abstract class BaseOperation {
     @observable public Error: string = "";
     @observable public OverridingFilters: FilterModel[] = [];
     //@observable 
-    public Result?: Result = undefined;
+    @observable public Result?: Result = undefined;
     @observable public ComputationStarted: boolean = false;
     public OperationReference?: OperationReference = undefined;
 
@@ -46,11 +46,8 @@ export abstract class BaseOperation {
     }
 
 
-    public YieldResult: ((result: Result) => void) | undefined;
     @action
     public SetResult(result: Result): void {
-        if (this.YieldResult)
-            this.YieldResult(result);
         this.Result = result;
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 8dbf80533..2dcf7fbbe 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -3,17 +3,15 @@ import { observer } from "mobx-react";
 import { Document } from "../../../../fields/Document";
 import { FieldWaiting } from "../../../../fields/Field";
 import { KeyStore } from "../../../../fields/KeyStore";
+import { ListField } from "../../../../fields/ListField";
 import { Utils } from "../../../../Utils";
 import { DocumentManager } from "../../../util/DocumentManager";
 import { DocumentView } from "../../nodes/DocumentView";
 import { CollectionViewProps } from "../CollectionViewBase";
 import "./CollectionFreeFormLinksView.scss";
+import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
 import React = require("react");
 import v5 = require("uuid/v5");
-import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
-import { ListField } from "../../../../fields/ListField";
-import { TextField } from "../../../../fields/TextField";
-import { StyleConstants } from "../../../northstar/utils/StyleContants";
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
@@ -31,36 +29,34 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
                     let x1w = srcDoc.GetNumber(KeyStore.Width, 0);
                     let x2 = dstDoc.GetNumber(KeyStore.X, 0);
                     let x2w = dstDoc.GetNumber(KeyStore.Width, 0);
-                    if (Math.abs(x1 + x1w - x2) < 20 || Math.abs(x2 + x2w - x1) < 20) {
-                        let linkDoc: Document = new Document();
-                        dstDoc.GetTAsync(KeyStore.Prototype, Document).then((protoDest) =>
-                            srcDoc.GetTAsync(KeyStore.Prototype, Document).then((protoSrc) => runInAction(() => {
-                                linkDoc.Set(KeyStore.Title, new TextField("New Brush"));
-                                linkDoc.Set(KeyStore.LinkDescription, new TextField(""));
-                                linkDoc.Set(KeyStore.LinkTags, new TextField("Default"));
-                                linkDoc.SetNumber(KeyStore.BackgroundColor, StyleConstants.BRUSH_COLORS[0]);
-
-                                let dstTarg = (protoDest ? protoDest : dstDoc);
-                                let srcTarg = (protoSrc ? protoSrc : srcDoc);
+                    dstDoc.GetTAsync(KeyStore.Prototype, Document).then((protoDest) =>
+                        srcDoc.GetTAsync(KeyStore.Prototype, Document).then((protoSrc) => runInAction(() => {
+                            let dstTarg = (protoDest ? protoDest : dstDoc);
+                            let srcTarg = (protoSrc ? protoSrc : srcDoc);
+                            let findBrush = (field: ListField<Document>) => field.Data.findIndex(brush => {
+                                let bdocs = brush.GetList(KeyStore.BrushingDocs, [] as Document[]);
+                                return (bdocs[0] == dstTarg && bdocs[1] == srcTarg) || (bdocs[0] == srcTarg && bdocs[1] == dstTarg)
+                            });
+                            let brushAction = (field: ListField<Document>) => {
+                                let found = findBrush(field);
+                                if (found != -1)
+                                    field.Data.splice(found, 1);
+                            };
+                            if (Math.abs(x1 + x1w - x2) < 20 || Math.abs(x2 + x2w - x1) < 20) {
+                                let linkDoc: Document = new Document();
+                                linkDoc.SetText(KeyStore.Title, "Histogram Brush");
+                                linkDoc.SetText(KeyStore.LinkDescription, "Brush between " + srcTarg.Title + " and " + dstTarg.Title);
                                 linkDoc.SetData(KeyStore.BrushingDocs, [dstTarg, srcTarg], ListField);
-                                dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) })
-                                srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) })
-                            }))
-                        )
-                    } else {
-                        dstDoc.GetTAsync(KeyStore.Prototype, Document).then((protoDest) =>
-                            srcDoc.GetTAsync(KeyStore.Prototype, Document).then((protoSrc) => runInAction(() => {
 
-                                let dstTarg = (protoDest ? protoDest : dstDoc);
-                                let srcTarg = (protoSrc ? protoSrc : srcDoc);
-                                dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, field => { (field as ListField<Document>).Data.length = 0 })
-                                srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, field => { (field as ListField<Document>).Data.length = 0 })
-                            }))
-                        )
-                    }
+                                brushAction = (field: ListField<Document>) => (findBrush(field) == -1) && field.Data.push(linkDoc);
+                            }
+                            dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
+                            srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
+                        }
+                        )))
                 }
             }
-        });
+        })
     }
     documentAnchors(view: DocumentView) {
         let equalViews = [view];
-- 
cgit v1.2.3-70-g09d2


From 74e95e596d9876e90c294ade30df3d666d167139 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 29 Mar 2019 23:48:34 -0400
Subject: fixed brush colors.

---
 src/client/northstar/dash-fields/HistogramField.ts             |  2 +-
 src/client/northstar/dash-nodes/HistogramBox.tsx               | 10 +++-------
 src/client/northstar/operations/HistogramOperation.ts          |  2 +-
 .../collectionFreeForm/CollectionFreeFormLinksView.tsx         | 10 ++++++----
 4 files changed, 11 insertions(+), 13 deletions(-)

(limited to 'src/client/northstar/dash-nodes/HistogramBox.tsx')

diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts
index ae83ea5ba..4a557e123 100644
--- a/src/client/northstar/dash-fields/HistogramField.ts
+++ b/src/client/northstar/dash-fields/HistogramField.ts
@@ -25,7 +25,7 @@ export class HistogramField extends BasicField<HistogramOperation> {
         return dup;
     }
     toString(): string {
-        return JSON.stringify(this.omitKeys(this.Data, ['Links', 'BrushLinks', 'Result']));
+        return JSON.stringify(this.omitKeys(this.Data, ['Links', 'BrushLinks', 'Result', 'BrushColors']));
     }
 
     Copy(): Field {
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index 65d917fd3..a968def96 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -124,16 +124,12 @@ export class HistogramBox extends React.Component<FieldViewProps> {
                     reaction(() => this.props.doc.GetList(KeyStore.BrushingDocs, []).length,
                         () => {
                             let brushingDocs = this.props.doc.GetList(KeyStore.BrushingDocs, [] as Document[]);
-                            var availableColors = StyleConstants.BRUSH_COLORS.map(c => c);
                             let proto = this.props.doc.GetPrototype() as Document;
-                            let brushingLinks = brushingDocs.map((brush, i) => {
-                                brush.SetNumber(KeyStore.BackgroundColor, availableColors[i % availableColors.length]);
+                            this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingDocs.map((brush, i) => {
+                                brush.SetNumber(KeyStore.BackgroundColor, StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length]);
                                 let brushed = brush.GetList(KeyStore.BrushingDocs, [] as Document[]);
-                                if (!brushed || brushed.length < 2)
-                                    return undefined;
                                 return { l: brush, b: brushed[0].Id == proto.Id ? brushed[1] : brushed[0] }
-                            }).filter(x => x != undefined) as { l: Document, b: Document }[];
-                            this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingLinks);
+                            }));
                         }, { fireImmediately: true });
                     reaction(() => this.createOperationParamsCache, () => this.HistoOp.Update(), { fireImmediately: true });
                 }
diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
index 4689cb233..e63de1632 100644
--- a/src/client/northstar/operations/HistogramOperation.ts
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -116,7 +116,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
 
     @action
     public async Update(): Promise<void> {
-        //this.BrushColors = this.BrushLinks.map(e => e.l.GetNumber(KeyStore.BackgroundColor, 0));
+        this.BrushColors = this.BrushLinks.map(e => e.l.GetNumber(KeyStore.BackgroundColor, 0));
         return super.Update();
     }
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 2dcf7fbbe..f793b3a49 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -26,16 +26,18 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
                     let srcDoc = views[j].props.Document;
                     let dstDoc = views[i].props.Document;
                     let x1 = srcDoc.GetNumber(KeyStore.X, 0);
-                    let x1w = srcDoc.GetNumber(KeyStore.Width, 0);
+                    let x1w = srcDoc.GetNumber(KeyStore.Width, -1);
                     let x2 = dstDoc.GetNumber(KeyStore.X, 0);
-                    let x2w = dstDoc.GetNumber(KeyStore.Width, 0);
+                    let x2w = dstDoc.GetNumber(KeyStore.Width, -1);
+                    if (x1w < 0 || x2w < 0)
+                        continue;
                     dstDoc.GetTAsync(KeyStore.Prototype, Document).then((protoDest) =>
                         srcDoc.GetTAsync(KeyStore.Prototype, Document).then((protoSrc) => runInAction(() => {
                             let dstTarg = (protoDest ? protoDest : dstDoc);
                             let srcTarg = (protoSrc ? protoSrc : srcDoc);
                             let findBrush = (field: ListField<Document>) => field.Data.findIndex(brush => {
                                 let bdocs = brush.GetList(KeyStore.BrushingDocs, [] as Document[]);
-                                return (bdocs[0] == dstTarg && bdocs[1] == srcTarg) || (bdocs[0] == srcTarg && bdocs[1] == dstTarg)
+                                return (bdocs.length == 0 || (bdocs[0] == dstTarg && bdocs[1] == srcTarg) || (bdocs[0] == srcTarg && bdocs[1] == dstTarg))
                             });
                             let brushAction = (field: ListField<Document>) => {
                                 let found = findBrush(field);
@@ -48,7 +50,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
                                 linkDoc.SetText(KeyStore.LinkDescription, "Brush between " + srcTarg.Title + " and " + dstTarg.Title);
                                 linkDoc.SetData(KeyStore.BrushingDocs, [dstTarg, srcTarg], ListField);
 
-                                brushAction = (field: ListField<Document>) => (findBrush(field) == -1) && field.Data.push(linkDoc);
+                                brushAction = brushAction = (field: ListField<Document>) => (findBrush(field) == -1) && field.Data.push(linkDoc);
                             }
                             dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
                             srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
-- 
cgit v1.2.3-70-g09d2


From 8448679e0a260ec881c54a10578bc464cd38f04e Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 1 Apr 2019 17:05:42 -0400
Subject: added dragging multiple items.

---
 src/client/northstar/dash-nodes/HistogramBox.tsx   |   4 +-
 src/client/util/DragManager.ts                     | 168 +++++++++++----------
 src/client/views/DocumentDecorations.scss          |   6 +
 src/client/views/DocumentDecorations.tsx           | 109 +++++++++----
 src/client/views/Main.tsx                          |   7 +-
 .../views/collections/CollectionDockingView.tsx    |  11 +-
 src/client/views/collections/CollectionView.tsx    |   2 +-
 .../views/collections/CollectionViewBase.tsx       |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  21 ++-
 src/client/views/nodes/DocumentView.tsx            |  15 +-
 10 files changed, 213 insertions(+), 134 deletions(-)

(limited to 'src/client/northstar/dash-nodes/HistogramBox.tsx')

diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index a968def96..49ebe5ebc 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -54,7 +54,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {
     @action
     dropX = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
-            let h = de.data.draggedDocument.GetT(KeyStore.Data, HistogramField);
+            let h = de.data.draggedDocuments[0].GetT(KeyStore.Data, HistogramField);
             if (h && h != FieldWaiting) {
                 this.HistoOp.X = h.Data.X;
             }
@@ -65,7 +65,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {
     @action
     dropY = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
-            let h = de.data.draggedDocument.GetT(KeyStore.Data, HistogramField);
+            let h = de.data.draggedDocuments[0].GetT(KeyStore.Data, HistogramField);
             if (h && h != FieldWaiting) {
                 this.HistoOp.Y = h.Data.X;
             }
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 70b1c9829..9ffe964ef 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -14,9 +14,9 @@ export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
 
         document.removeEventListener("pointermove", onRowMove);
         document.removeEventListener('pointerup', onRowUp);
-        var dragData = new DragManager.DocumentDragData(docFunc());
+        var dragData = new DragManager.DocumentDragData([docFunc()]);
         dragData.removeDocument = removeFunc;
-        DragManager.StartDocumentDrag(_reference.current!, dragData);
+        DragManager.StartDocumentDrag([_reference.current!], dragData);
     });
     let onRowUp = action((e: PointerEvent): void => {
         document.removeEventListener("pointermove", onRowMove);
@@ -27,7 +27,7 @@ export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
         if (e.button == 0) {
             e.stopPropagation();
             if (e.shiftKey) {
-                CollectionDockingView.Instance.StartOtherDrag(docFunc(), e);
+                CollectionDockingView.Instance.StartOtherDrag([docFunc()], e);
             } else {
                 document.addEventListener("pointermove", onRowMove);
                 document.addEventListener('pointerup', onRowUp);
@@ -101,20 +101,21 @@ export namespace DragManager {
     }
 
     export class DocumentDragData {
-        constructor(dragDoc: Document) {
-            this.draggedDocument = dragDoc;
-            this.droppedDocument = dragDoc;
+        constructor(dragDoc: Document[]) {
+            this.draggedDocuments = dragDoc;
+            this.droppedDocuments = dragDoc;
         }
-        draggedDocument: Document;
-        droppedDocument: Document;
+        draggedDocuments: Document[];
+        droppedDocuments: Document[];
         xOffset?: number;
         yOffset?: number;
         aliasOnDrop?: boolean;
         removeDocument?: (collectionDrop: CollectionView) => void;
         [id: string]: any;
     }
-    export function StartDocumentDrag(ele: HTMLElement, dragData: DocumentDragData, options?: DragOptions) {
-        StartDrag(ele, dragData, options, (dropData: { [id: string]: any }) => dropData.droppedDocument = dragData.aliasOnDrop ? dragData.draggedDocument.CreateAlias() : dragData.draggedDocument);
+
+    export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, options?: DragOptions) {
+        StartDrag(eles, dragData, options, (dropData: { [id: string]: any }) => dropData.droppedDocuments = dragData.aliasOnDrop ? dragData.draggedDocuments.map(d => d.CreateAlias()) : dragData.draggedDocuments);
     }
 
     export class LinkDragData {
@@ -125,49 +126,58 @@ export namespace DragManager {
         [id: string]: any;
     }
     export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, options?: DragOptions) {
-        StartDrag(ele, dragData, options);
+        StartDrag([ele], dragData, options);
     }
-    function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
+    function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
         if (!dragDiv) {
             dragDiv = document.createElement("div");
             dragDiv.className = "dragManager-dragDiv"
             DragManager.Root().appendChild(dragDiv);
         }
-        const w = ele.offsetWidth, h = ele.offsetHeight;
-        const rect = ele.getBoundingClientRect();
-        const scaleX = rect.width / w, scaleY = rect.height / h;
-        let x = rect.left, y = rect.top;
-        // const offsetX = e.x - rect.left, offsetY = e.y - rect.top;
-
-        let dragElement = ele.cloneNode(true) as HTMLElement;
-        dragElement.style.opacity = "0.7";
-        dragElement.style.position = "absolute";
-        dragElement.style.bottom = "";
-        dragElement.style.left = "";
-        dragElement.style.transformOrigin = "0 0";
-        dragElement.style.zIndex = "1000";
-        dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
-        dragElement.style.width = `${rect.width / scaleX}px`;
-        dragElement.style.height = `${rect.height / scaleY}px`;
-
-        // bcz: PDFs don't show up if you clone them because they contain a canvas.
-        //      however, PDF's have a thumbnail field that contains an image of their canvas.
-        //      So we replace the pdf's canvas with the image thumbnail
-        const doc: Document = dragData["draggedDocument"];
-        if (doc) {
-            var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement;
-            let thumbnail = doc.GetT(KeyStore.Thumbnail, ImageField);
-            if (pdfBox && pdfBox.childElementCount && thumbnail) {
-                let img = new Image();
-                img!.src = thumbnail.toString();
-                img!.style.position = "absolute";
-                img!.style.width = `${rect.width / scaleX}px`;
-                img!.style.height = `${rect.height / scaleY}px`;
-                pdfBox.replaceChild(img!, pdfBox.children[0])
+
+        let scaleXs: number[] = [];
+        let scaleYs: number[] = [];
+        let xs: number[] = [];
+        let ys: number[] = [];
+
+        const docs: Document[] = dragData instanceof DocumentDragData ? dragData.draggedDocuments : [];
+        let dragElements = eles.map(ele => {
+            const w = ele.offsetWidth, h = ele.offsetHeight;
+            const rect = ele.getBoundingClientRect();
+            const scaleX = rect.width / w, scaleY = rect.height / h;
+            let x = rect.left, y = rect.top;
+            xs.push(x); ys.push(y);
+            scaleXs.push(scaleX); scaleYs.push(scaleY);
+            let dragElement = ele.cloneNode(true) as HTMLElement;
+            dragElement.style.opacity = "0.7";
+            dragElement.style.position = "absolute";
+            dragElement.style.bottom = "";
+            dragElement.style.left = "";
+            dragElement.style.transformOrigin = "0 0";
+            dragElement.style.zIndex = "1000";
+            dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
+            dragElement.style.width = `${rect.width / scaleX}px`;
+            dragElement.style.height = `${rect.height / scaleY}px`;
+
+            // bcz: PDFs don't show up if you clone them because they contain a canvas.
+            //      however, PDF's have a thumbnail field that contains an image of their canvas.
+            //      So we replace the pdf's canvas with the image thumbnail
+            if (docs.length) {
+                var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement;
+                let thumbnail = docs[0].GetT(KeyStore.Thumbnail, ImageField);
+                if (pdfBox && pdfBox.childElementCount && thumbnail) {
+                    let img = new Image();
+                    img!.src = thumbnail.toString();
+                    img!.style.position = "absolute";
+                    img!.style.width = `${rect.width / scaleX}px`;
+                    img!.style.height = `${rect.height / scaleY}px`;
+                    pdfBox.replaceChild(img!, pdfBox.children[0])
+                }
             }
-        }
 
-        dragDiv.appendChild(dragElement);
+            dragDiv.appendChild(dragElement);
+            return dragElement;
+        });
 
         let hideSource = false;
         if (options) {
@@ -177,62 +187,64 @@ export namespace DragManager {
                 hideSource = options.hideSource();
             }
         }
-        const wasHidden = ele.hidden;
-        if (hideSource) {
-            ele.hidden = true;
-        }
+        eles.map(ele => ele.hidden = hideSource);
+
         const moveHandler = (e: PointerEvent) => {
             e.stopPropagation();
             e.preventDefault();
-            x += e.movementX;
-            y += e.movementY;
             if (dragData instanceof DocumentDragData)
                 dragData.aliasOnDrop = e.ctrlKey || e.altKey;
             if (e.shiftKey) {
                 abortDrag();
-                CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 });
+                CollectionDockingView.Instance.StartOtherDrag(docs, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 });
             }
-            dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
+            dragElements.map((dragElement, i) => dragElement.style.transform = `translate(${xs[i] += e.movementX}px, ${ys[i] += e.movementY}px) scale(${scaleXs[i]}, ${scaleYs[i]})`);
         };
 
         const abortDrag = () => {
             document.removeEventListener("pointermove", moveHandler, true);
             document.removeEventListener("pointerup", upHandler);
-            dragDiv.removeChild(dragElement);
-            ele.hidden = false;
+            dragElements.map(dragElement => dragDiv.removeChild(dragElement));
+            eles.map(ele => ele.hidden = false);
         }
         const upHandler = (e: PointerEvent) => {
             abortDrag();
-            FinishDrag(ele, e, dragData, options, finishDrag);
+            FinishDrag(eles, e, dragData, options, finishDrag);
         };
         document.addEventListener("pointermove", moveHandler, true);
         document.addEventListener("pointerup", upHandler);
     }
 
-    function FinishDrag(dragEle: HTMLElement, e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
-        let parent = dragEle.parentElement;
-        if (parent)
-            parent.removeChild(dragEle);
+    function FinishDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
+        let removed = dragEles.map(dragEle => {
+            let parent = dragEle.parentElement;
+            if (parent)
+                parent.removeChild(dragEle);
+            return [dragEle, parent];
+        });
         const target = document.elementFromPoint(e.x, e.y);
-        if (parent)
-            parent.appendChild(dragEle);
-        if (!target) {
-            return;
-        }
-        if (finishDrag)
-            finishDrag(dragData);
-
-        target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", {
-            bubbles: true,
-            detail: {
-                x: e.x,
-                y: e.y,
-                data: dragData
+        removed.map(r => {
+            let dragEle: HTMLElement = r[0]!;
+            let parent: HTMLElement | null = r[1];
+            if (parent)
+                parent.appendChild(dragEle);
+        });
+        if (target) {
+            if (finishDrag)
+                finishDrag(dragData);
+
+            target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", {
+                bubbles: true,
+                detail: {
+                    x: e.x,
+                    y: e.y,
+                    data: dragData
+                }
+            }));
+
+            if (options) {
+                options.handlers.dragComplete({});
             }
-        }));
-
-        if (options) {
-            options.handlers.dragComplete({});
         }
         DocumentDecorations.Instance.Hidden = false;
     }
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 11595aa01..7a43f3087 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -32,6 +32,12 @@
     }
 }
 
+.documentDecorations-background {
+    background:lightblue;
+    position: absolute;
+    opacity: 0.1;
+}
+
 //     position: absolute;
 //     display: grid;
 //     z-index: 1000;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 47098c3b5..faba3b6ea 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,14 +1,14 @@
-import { observable, computed, action } from "mobx";
-import React = require("react");
-import { SelectionManager } from "../util/SelectionManager";
+import { action, computed, observable, trace } from "mobx";
 import { observer } from "mobx-react";
-import './DocumentDecorations.scss'
-import { KeyStore } from '../../fields/KeyStore'
+import { KeyStore } from '../../fields/KeyStore';
+import { ListField } from "../../fields/ListField";
 import { NumberField } from "../../fields/NumberField";
-import { props } from "bluebird";
 import { DragManager } from "../util/DragManager";
+import { SelectionManager } from "../util/SelectionManager";
+import { CollectionView } from "./collections/CollectionView";
+import './DocumentDecorations.scss';
 import { LinkMenu } from "./nodes/LinkMenu";
-import { ListField } from "../../fields/ListField";
+import React = require("react");
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -22,6 +22,7 @@ export class DocumentDecorations extends React.Component {
     private _resizeBorderWidth = 16;
     private _linkButton = React.createRef<HTMLDivElement>();
     @observable private _hidden = false;
+    @observable private _dragging = false;
 
     constructor(props: Readonly<{}>) {
         super(props)
@@ -50,6 +51,50 @@ export class DocumentDecorations extends React.Component {
     public get Hidden() { return this._hidden; }
     public set Hidden(value: boolean) { this._hidden = value; }
 
+    _lastDrag: number[] = [0, 0];
+    onBackgroundDown = (e: React.PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onBackgroundMove);
+        document.addEventListener("pointermove", this.onBackgroundMove);
+        document.removeEventListener("pointerup", this.onBackgroundUp);
+        document.addEventListener("pointerup", this.onBackgroundUp);
+        this._lastDrag = [e.clientX, e.clientY]
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
+    @action
+    onBackgroundMove = (e: PointerEvent): void => {
+        let dragDocView = SelectionManager.SelectedDocuments()[0];
+        const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+        let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
+        dragData.aliasOnDrop = false;
+        dragData.xOffset = e.x - left;
+        dragData.yOffset = e.y - top;
+        dragData.removeDocument = (dropCollectionView: CollectionView) =>
+            dragData.draggedDocuments.map(d => {
+                if (dragDocView.props.RemoveDocument && dragDocView.props.ContainingCollectionView !== dropCollectionView) {
+                    dragDocView.props.RemoveDocument(d);
+                }
+            });
+        this._dragging = true;
+        document.removeEventListener("pointermove", this.onBackgroundMove);
+        document.removeEventListener("pointerup", this.onBackgroundUp);
+        DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => (docView as any)._mainCont!.current!), dragData, {
+            handlers: {
+                dragComplete: action(() => this._dragging = false),
+            },
+            hideSource: true
+        })
+        e.stopPropagation();
+    }
+
+    onBackgroundUp = (e: PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onBackgroundMove);
+        document.removeEventListener("pointerup", this.onBackgroundUp);
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
     onPointerDown = (e: React.PointerEvent): void => {
         e.stopPropagation();
         if (e.button === 0) {
@@ -191,7 +236,6 @@ export class DocumentDecorations extends React.Component {
     // buttonOnPointerUp = (e: React.PointerEvent): void => {
     //     e.stopPropagation();
     // }
-
     render() {
         var bounds = this.Bounds;
         if (this.Hidden) {
@@ -218,25 +262,36 @@ export class DocumentDecorations extends React.Component {
             </Flyout>);
         }
         return (
-            <div id="documentDecorations-container" style={{
-                width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
-                height: (bounds.b - bounds.y + this._resizeBorderWidth + 30) + "px",
-                left: bounds.x - this._resizeBorderWidth / 2,
-                top: bounds.y - this._resizeBorderWidth / 2,
-            }}>
-                <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-leftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-centerCont"></div>
-                <div id="documentDecorations-rightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-
-                <div title="View Links" className="linkFlyout" ref={this._linkButton}>{linkButton}</div>
-
-            </div >
+            <div className="documentDecorations">
+                <div className="documentDecorations-background" style={{
+                    width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+                    height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px",
+                    left: bounds.x - this._resizeBorderWidth / 2,
+                    top: bounds.y - this._resizeBorderWidth / 2,
+                    pointerEvents: this._dragging ? "none" : "all",
+                    zIndex: SelectionManager.SelectedDocuments().length > 1 ? 1000 : 0,
+                }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation() }} >
+                </div>
+                <div id="documentDecorations-container" style={{
+                    width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+                    height: (bounds.b - bounds.y + this._resizeBorderWidth + 30) + "px",
+                    left: bounds.x - this._resizeBorderWidth / 2,
+                    top: bounds.y - this._resizeBorderWidth / 2,
+                }}>
+                    <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-leftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-centerCont"></div>
+                    <div id="documentDecorations-rightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+
+                    <div title="View Links" className="linkFlyout" ref={this._linkButton}>{linkButton}</div>
+
+                </div >
+            </div>
         )
     }
 }
\ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 75855c3d1..6f66f8f38 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -218,7 +218,8 @@ export class Main extends React.Component {
     focusDocument = (doc: Document) => { }
     noScaling = () => 1;
 
-    get content() {
+    @computed
+    get mainContent() {
         return !this.mainContainer ? (null) :
             <DocumentView Document={this.mainContainer}
                 AddDocument={undefined}
@@ -311,17 +312,17 @@ export class Main extends React.Component {
         }
         return (
             <div id="main-div">
+                <DocumentDecorations />
                 <Measure onResize={(r: any) => runInAction(() => {
                     this.pwidth = r.entry.width;
                     this.pheight = r.entry.height;
                 })}>
                     {({ measureRef }) =>
                         <div ref={measureRef} id="mainContent-div">
-                            {this.content}
+                            {this.mainContent}
                         </div>
                     }
                 </Measure>
-                <DocumentDecorations />
                 <ContextMenu />
                 {this.nodesMenu}
                 {this.miscButtons}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index f123149dd..c6cbc05e7 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -47,9 +47,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         (window as any).React = React;
         (window as any).ReactDOM = ReactDOM;
     }
-    public StartOtherDrag(dragDoc: Document, e: any) {
-        this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
-            onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 })
+    public StartOtherDrag(dragDocs: Document[], e: any) {
+        dragDocs.map(dragDoc =>
+            this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
+                onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }));
     }
 
     @action
@@ -199,7 +200,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             let tab = (e.target as any).parentElement as HTMLElement;
             Server.GetField(docid, action((f: Opt<Field>) => {
                 if (f instanceof Document)
-                    DragManager.StartDocumentDrag(tab, new DragManager.DocumentDragData(f as Document),
+                    DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f as Document]),
                         {
                             handlers: {
                                 dragComplete: action(() => { }),
@@ -265,6 +266,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
 
     render() {
+        trace();
         return (
             <div className="collectiondockingview-container" id="menuContainer"
                 onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef}
@@ -305,6 +307,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
 
     render() {
+        trace();
         if (!this._document)
             return (null);
         var content =
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index a1498e0ae..014aa1d8f 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -113,7 +113,7 @@ export class CollectionView extends React.Component<CollectionViewProps> {
         if (index !== -1) {
             value.splice(index, 1)
 
-            SelectionManager.DeselectAll()
+            //SelectionManager.DeselectAll()
             ContextMenu.Instance.clearItems()
             return true;
         }
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 987f3cb6c..316d20c9d 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -82,9 +82,9 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         if (de.data instanceof DragManager.DocumentDragData) {
             if (de.data.aliasOnDrop) {
                 [KeyStore.Width, KeyStore.Height, KeyStore.CurPage].map(key =>
-                    de.data.draggedDocument.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocument.SetNumber(key, f.Data) : null));
+                    de.data.draggedDocuments.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocument.SetNumber(key, f.Data) : null));
             }
-            let added = this.props.addDocument(de.data.droppedDocument, false);
+            let added = de.data.droppedDocuments.reduce((added, d) => this.props.addDocument(d, false), true);
             if (added && de.data.removeDocument && !de.data.aliasOnDrop) {
                 de.data.removeDocument(this.props.CollectionView);
             }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 60fb95ff5..c5178f69d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -77,13 +77,20 @@ export class CollectionFreeFormView extends CollectionViewBase {
                 let screenX = de.x - (de.data.xOffset as number || 0);
                 let screenY = de.y - (de.data.yOffset as number || 0);
                 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);
+                let dragDoc = de.data.draggedDocuments[0];
+                let dragX = dragDoc.GetNumber(KeyStore.X, 0);
+                let dragY = dragDoc.GetNumber(KeyStore.Y, 0);
+                de.data.draggedDocuments.map(d => {
+                    let docX = d.GetNumber(KeyStore.X, 0);
+                    let docY = d.GetNumber(KeyStore.Y, 0);
+                    d.SetNumber(KeyStore.X, x + (docX - dragX));
+                    d.SetNumber(KeyStore.Y, y + (docY - dragY));
+                    if (!d.GetNumber(KeyStore.Width, 0)) {
+                        d.SetNumber(KeyStore.Width, 300);
+                        d.SetNumber(KeyStore.Height, 300);
+                    }
+                    this.bringToFront(d);
+                })
             }
             return true;
         }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 71613ca4f..1195128dc 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -106,7 +106,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
             if (this.props.isTopMost) {
                 this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey);
             }
-            else CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e);
+            else CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e);
             e.stopPropagation();
         } else {
             if (this.active && !e.isDefaultPrevented()) {
@@ -125,9 +125,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         if (this._mainCont.current) {
             this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } });
         }
-        runInAction(() => {
-            DocumentManager.Instance.DocumentViews.push(this);
-        })
+        runInAction(() => DocumentManager.Instance.DocumentViews.push(this))
         this._reactionDisposer = reaction(
             () => this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.slice(),
             () => {
@@ -149,10 +147,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         if (this.dropDisposer) {
             this.dropDisposer();
         }
-        runInAction(() => {
-            DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
-
-        })
+        runInAction(() => DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1))
         if (this._reactionDisposer) {
             this._reactionDisposer();
         }
@@ -161,7 +156,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) {
         if (this._mainCont.current) {
             const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
-            let dragData = new DragManager.DocumentDragData(this.props.Document);
+            let dragData = new DragManager.DocumentDragData([this.props.Document]);
             dragData.aliasOnDrop = dropAliasOfDraggedDoc;
             dragData.xOffset = x - left;
             dragData.yOffset = y - top;
@@ -170,7 +165,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                     this.props.RemoveDocument(this.props.Document);
                 }
             }
-            DragManager.StartDocumentDrag(this._mainCont.current, dragData, {
+            DragManager.StartDocumentDrag([this._mainCont.current], dragData, {
                 handlers: {
                     dragComplete: action(() => { }),
                 },
-- 
cgit v1.2.3-70-g09d2

