From b8c0dc61d535caeb2142678dffea2a841776e3a8 Mon Sep 17 00:00:00 2001 From: bob Date: Wed, 13 Mar 2019 16:39:45 -0400 Subject: moved DocumentContents into DocumentContentsView file. --- src/client/views/nodes/DocumentContentsView.tsx | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/client/views/nodes/DocumentContentsView.tsx (limited to 'src/client/views/nodes/DocumentContentsView.tsx') diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx new file mode 100644 index 000000000..55b4938a0 --- /dev/null +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -0,0 +1,35 @@ +import { Document } from "../../../fields/Document"; +import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionSchemaView } from "../collections/CollectionSchemaView"; +import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { ImageBox } from "../nodes/ImageBox"; +import { VideoBox } from "../nodes/VideoBox"; +import { AudioBox } from "../nodes/AudioBox"; +import { KeyValueBox } from "./KeyValueBox" +import { WebBox } from "../nodes/WebBox"; +import { PDFBox } from "../nodes/PDFBox"; +import "./DocumentView.scss"; +import React = require("react"); +const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? + +interface JsxBindings { + Document: Document; + layout: string; + [prop: string]: any; +} + +export class DocumentContentsView extends React.PureComponent { + render() { + return { console.log(test) }} + /> + } +} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 1bf3a5a0108704a0204cedcbad6e6d52d19cf982 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sun, 17 Mar 2019 22:04:03 -0400 Subject: moved DocumentContents to DocumentContentsView --- src/client/views/InkingControl.tsx | 8 +++ .../views/collections/CollectionFreeFormView.tsx | 34 ++---------- src/client/views/collections/MarqueeView.tsx | 2 +- src/client/views/collections/PreviewCursor.tsx | 10 ++-- src/client/views/nodes/DocumentContentsView.tsx | 60 +++++++++++++++------- src/client/views/nodes/DocumentView.tsx | 55 ++------------------ 6 files changed, 62 insertions(+), 107 deletions(-) (limited to 'src/client/views/nodes/DocumentContentsView.tsx') diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index fb75ef2a5..6616f68d8 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -9,6 +9,8 @@ import "./InkingCanvas.scss" import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons'; +import { SelectionManager } from "../util/SelectionManager"; +import { KeyStore } from "../../fields/KeyStore"; library.add(faPen, faHighlighter, faEraser, faBan); @@ -34,6 +36,12 @@ export class InkingControl extends React.Component { @action switchColor = (color: ColorResult): void => { this._selectedColor = color.hex; + if (SelectionManager.SelectedDocuments().length == 1) { + var sdoc = SelectionManager.SelectedDocuments()[0]; + if (sdoc.props.ContainingCollectionView && sdoc.props.ContainingCollectionView) { + sdoc.props.Document.SetText(KeyStore.BackgroundColor, color.hex); + } + } } @action diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 988c9941f..cca57912c 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -8,28 +8,16 @@ import { TextField } from "../../../fields/TextField"; import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionPDFView } from "../collections/CollectionPDFView"; -import { CollectionSchemaView } from "../collections/CollectionSchemaView"; -import { CollectionVideoView } from "../collections/CollectionVideoView"; -import { CollectionView } from "../collections/CollectionView"; import { InkingCanvas } from "../InkingCanvas"; -import { AudioBox } from "../nodes/AudioBox"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; -import { DocumentView, DocumentViewProps, DocumentContents } from "../nodes/DocumentView"; -import { FormattedTextBox } from "../nodes/FormattedTextBox"; -import { ImageBox } from "../nodes/ImageBox"; -import { KeyValueBox } from "../nodes/KeyValueBox"; -import { PDFBox } from "../nodes/PDFBox"; -import { VideoBox } from "../nodes/VideoBox"; -import { WebBox } from "../nodes/WebBox"; +import { DocumentContentsView } from "../nodes/DocumentContentsView"; +import { DocumentView, DocumentViewProps } from "../nodes/DocumentView"; import "./CollectionFreeFormView.scss"; import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { CollectionViewBase } from "./CollectionViewBase"; import { MarqueeView } from "./MarqueeView"; import { PreviewCursor } from "./PreviewCursor"; import React = require("react"); -const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? @observer export class CollectionFreeFormView extends CollectionViewBase { @@ -283,28 +271,14 @@ export class CollectionFreeFormView extends CollectionViewBase { @computed get backgroundView() { return !this.backgroundLayout ? (null) : - ( false} select={() => { }} />); - // ( console.log(test)} - // />); } @computed get overlayView() { return !this.overlayLayout ? (null) : - ( false} select={() => { }} />); - // ( console.log(test)} - // />); } getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform()) diff --git a/src/client/views/collections/MarqueeView.tsx b/src/client/views/collections/MarqueeView.tsx index 65aaa837f..f5c83a934 100644 --- a/src/client/views/collections/MarqueeView.tsx +++ b/src/client/views/collections/MarqueeView.tsx @@ -121,7 +121,7 @@ export class MarqueeView extends React.Component let centerShiftX = 0 - (selRect.left + selRect.width / 2); // moves each point by the offset that shifts the selection's center to the origin. let centerShiftY = 0 - (selRect.top + selRect.height / 2); let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); - if (ink && ink != FieldWaiting) { + if (ink && ink != FieldWaiting && ink.Data) { let idata = new Map(); ink.Data.forEach((value: StrokeData, key: string, map: any) => { let inside = InkingCanvas.IntersectStrokeRect(value, selRect); diff --git a/src/client/views/collections/PreviewCursor.tsx b/src/client/views/collections/PreviewCursor.tsx index a1411250a..cbcfa568d 100644 --- a/src/client/views/collections/PreviewCursor.tsx +++ b/src/client/views/collections/PreviewCursor.tsx @@ -1,16 +1,12 @@ -import { trace } from "mobx"; -import "./PreviewCursor.scss"; -import React = require("react"); import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; -import { FieldWaiting, Opt } from "../../../fields/Field"; -import { KeyStore } from "../../../fields/KeyStore"; -import { ListField } from "../../../fields/ListField"; +import { Opt } from "../../../fields/Field"; import { Documents } from "../../documents/Documents"; -import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import "./PreviewCursor.scss"; +import React = require("react"); export interface PreviewCursorProps { diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 55b4938a0..ce72ab64b 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,33 +1,55 @@ -import { Document } from "../../../fields/Document"; -import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; +import { computed } from "mobx"; +import { observer } from "mobx-react"; +import { FieldWaiting } from "../../../fields/Field"; +import { Key } from "../../../fields/Key"; +import { KeyStore } from "../../../fields/KeyStore"; +import { ListField } from "../../../fields/ListField"; import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionSchemaView } from "../collections/CollectionSchemaView"; -import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; -import { FormattedTextBox } from "../nodes/FormattedTextBox"; -import { ImageBox } from "../nodes/ImageBox"; -import { VideoBox } from "../nodes/VideoBox"; -import { AudioBox } from "../nodes/AudioBox"; -import { KeyValueBox } from "./KeyValueBox" -import { WebBox } from "../nodes/WebBox"; -import { PDFBox } from "../nodes/PDFBox"; +import { CollectionView } from "../collections/CollectionView"; +import { AudioBox } from "./AudioBox"; +import { DocumentViewProps, JsxBindings } from "./DocumentView"; import "./DocumentView.scss"; +import { FormattedTextBox } from "./FormattedTextBox"; +import { ImageBox } from "./ImageBox"; +import { KeyValueBox } from "./KeyValueBox"; +import { PDFBox } from "./PDFBox"; +import { VideoBox } from "./VideoBox"; +import { WebBox } from "./WebBox"; import React = require("react"); const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? -interface JsxBindings { - Document: Document; - layout: string; - [prop: string]: any; -} -export class DocumentContentsView extends React.PureComponent { +@observer +export class DocumentContentsView extends React.Component boolean, + select: (ctrl: boolean) => void, + layoutKey: Key +}> { + @computed get layout(): string { return this.props.Document.GetText(this.props.layoutKey, "

Error loading layout data

"); } + @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array()); } + @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array()); } + + CreateBindings(): JsxBindings { + let bindings: JsxBindings = { ...this.props, }; + for (const key of this.layoutKeys) { + bindings[key.Name + "Key"] = key; // this maps string values of the form Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data + } + for (const key of this.layoutFields) { + let field = this.props.Document.Get(key); + bindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; + } + return bindings; + } + render() { return { console.log(test) }} /> diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3e691ee6d..b36d47b8b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,11 +1,12 @@ import { action, computed, IReactionDisposer, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; -import { Field, FieldWaiting, Opt } from "../../../fields/Field"; +import { Field, Opt } from "../../../fields/Field"; import { Key } from "../../../fields/Key"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; import { TextField } from "../../../fields/TextField"; +import { Utils } from "../../../Utils"; import { Documents } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; @@ -14,22 +15,9 @@ import { Transform } from "../../util/Transform"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionView, CollectionViewType } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; +import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); -import { DocumentContentsView } from "./DocumentContentsView"; -import { Utils } from "../../../Utils"; -import { FormattedTextBox } from "./FormattedTextBox"; -import { ImageBox } from "./ImageBox"; -import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; -import { CollectionSchemaView } from "../collections/CollectionSchemaView"; -import { CollectionPDFView } from "../collections/CollectionPDFView"; -import { CollectionVideoView } from "../collections/CollectionVideoView"; -import { WebBox } from "./WebBox"; -import { KeyValueBox } from "./KeyValueBox"; -import { PDFBox } from "./PDFBox"; -import { VideoBox } from "./VideoBox"; -import { AudioBox } from "./AudioBox"; -const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? export interface DocumentViewProps { @@ -87,7 +75,7 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { return args; } -interface JsxBindings { +export interface JsxBindings { Document: Document; isSelected: () => boolean; select: (isCtrlPressed: boolean) => void; @@ -96,40 +84,7 @@ interface JsxBindings { [prop: string]: any; } -@observer -export class DocumentContents extends React.Component boolean, - select: (ctrl: boolean) => void, - layoutKey: Key -}> { - @computed get layout(): string { return this.props.Document.GetText(this.props.layoutKey, "

Error loading layout data

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

; + } return { console.log(test) }} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index fec451b09..cfe67c8fe 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -314,10 +314,6 @@ export class DocumentView extends React.Component { if (!this.props.Document) { return (null); } - let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField); - if (!lkeys || lkeys === FieldWaiting) { - return

Error loading layout keys

; - } var scaling = this.props.ContentScaling(); var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); -- cgit v1.2.3-70-g09d2 From e9826e0ac334bac28d173f67b4f0db0800b40680 Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 26 Mar 2019 16:06:06 -0400 Subject: cleaned up a bit. got rid of blinking when histos change. --- src/client/northstar/core/filter/FilterModel.ts | 50 +-- src/client/northstar/operations/BaseOperation.ts | 3 +- .../northstar/operations/HistogramOperation.ts | 1 - src/client/views/nodes/DocumentContentsView.tsx | 3 +- src/client/views/nodes/HistogramBox.tsx | 360 ++------------------ src/client/views/nodes/HistogramBoxPrimitives.tsx | 373 +++++++++++++++++++++ 6 files changed, 399 insertions(+), 391 deletions(-) create mode 100644 src/client/views/nodes/HistogramBoxPrimitives.tsx (limited to 'src/client/views/nodes/DocumentContentsView.tsx') diff --git a/src/client/northstar/core/filter/FilterModel.ts b/src/client/northstar/core/filter/FilterModel.ts index a9c79d245..bc7938947 100644 --- a/src/client/northstar/core/filter/FilterModel.ts +++ b/src/client/northstar/core/filter/FilterModel.ts @@ -1,11 +1,9 @@ import { ValueComparison } from "./ValueComparision"; import { Utils } from "../../utils/Utils"; -import { IBaseFilterProvider, instanceOfIBaseFilterProvider } from "./IBaseFilterProvider"; -import { BaseOperation } from "../../operations/BaseOperation"; +import { IBaseFilterProvider } from "./IBaseFilterProvider"; import { FilterOperand } from "./FilterOperand"; import { HistogramField } from "../../../../fields/HistogramField"; import { KeyStore } from "../../../../fields/KeyStore"; -import { filter } from "bluebird"; import { FieldWaiting } from "../../../../fields/Field"; import { Document } from "../../../../fields/Document"; @@ -35,8 +33,7 @@ export class FilterModel { } public ToPythonString(): string { - let ret = "(" + this.ValueComparisons.map(vc => vc.ToPythonString()).join("&&") + ")"; - return ret; + return "(" + this.ValueComparisons.map(vc => vc.ToPythonString()).join("&&") + ")"; } public static And(filters: string[]): string { @@ -84,47 +81,4 @@ export class FilterModel { } return ret; } - - // public static GetFilterModelsRecursive(baseOperation: BaseOperation, - // visitedFilterProviders: Set, filterModels: FilterModel[], isFirst: boolean): string { - // let ret = ""; - // if (Utils.isBaseFilterProvider(baseOperation)) { - // visitedFilterProviders.add(baseOperation); - // let filtered = baseOperation.FilterModels.filter(fm => fm && fm.ValueComparisons.length > 0); - // if (!isFirst && filtered.length > 0) { - // filterModels.push(...filtered); - // ret = "(" + baseOperation.FilterModels.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")"; - // } - // } - // if (Utils.isBaseFilterConsumer(baseOperation) && baseOperation.Links) { - // let children = new Array(); - // let linkedGraphNodes = baseOperation.Links.get(LinkType.Filter); - // if (linkedGraphNodes != null) { - // for (let i = 0; i < linkedGraphNodes.length; i++) { - // let linkVm = linkedGraphNodes[i].Data; - // let linkedGraphNode = linkedGraphNodes[i].Target; - // if (!visitedFilterProviders.has(linkedGraphNode)) { - // let child = FilterModel.GetFilterModelsRecursive(linkedGraphNode, visitedFilterProviders, filterModels, false); - // if (child !== "") { - // if (linkVm.IsInverted) { - // child = "! " + child; - // } - // children.push(child); - // } - // } - // } - // } - - // let childrenJoined = children.join(baseOperation.FilterOperand === FilterOperand.AND ? " && " : " || "); - // if (children.length > 0) { - // if (ret !== "") { - // ret = "(" + ret + " && (" + childrenJoined + "))"; - // } - // else { - // ret = "(" + childrenJoined + ")"; - // } - // } - // } - // return ret; - // } } \ No newline at end of file diff --git a/src/client/northstar/operations/BaseOperation.ts b/src/client/northstar/operations/BaseOperation.ts index 4bc8873d5..7db6fcb91 100644 --- a/src/client/northstar/operations/BaseOperation.ts +++ b/src/client/northstar/operations/BaseOperation.ts @@ -61,7 +61,8 @@ export abstract class BaseOperation { } let operationParameters = this.CreateOperationParameters(); - this.Result = undefined; + if (this.Result) + this.Result.progress = 0; // bcz: used to set Result to undefined, but that causes the display to blink this.Error = ""; let salt = Math.random().toString(); this.RequestSalt = salt; diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts index 93946b296..bceadb961 100644 --- a/src/client/northstar/operations/HistogramOperation.ts +++ b/src/client/northstar/operations/HistogramOperation.ts @@ -55,7 +55,6 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons public get FilterString(): string { let filterModels: FilterModel[] = []; let fstring = FilterModel.GetFilterModelsRecursive(this, new Set(), filterModels, true) - console.log("Filter string " + this.X.AttributeModel.DisplayName + " = " + fstring); return fstring; } @computed diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 12d14eb42..40990b76a 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -20,6 +20,7 @@ import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import { HistogramBox } from "./HistogramBox"; +import { HistogramBoxPrimitives } from "./HistogramBoxPrimitives"; import React = require("react"); const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -52,7 +53,7 @@ export class DocumentContentsView extends React.ComponentError loading layout keys

; } return { @@ -41,6 +40,7 @@ export class HistogramBox extends React.Component { @computed get xaxislines() { return this.renderGridLinesAndLabels(0); } @computed get yaxislines() { return this.renderGridLinesAndLabels(1); } + @computed get createOperationParamsCache() { return this.HistoOp!.CreateOperationParameters(); } componentDidMount() { reaction(() => [CurrentUserUtils.ActiveSchemaName, this.props.doc.GetText(KeyStore.NorthstarSchema, "?")], @@ -57,8 +57,8 @@ export class HistogramBox extends React.Component { binRanges[1] instanceof AggregateBinRange ? ChartType.VerticalBar : ChartType.HeatMap; this.VisualBinRanges.length = 0; - this.VisualBinRanges.push(VisualBinRangeHelper.GetVisualBinRange(binRanges[0], this.HistoOp.Result, this.HistoOp.X, this.ChartType)); - this.VisualBinRanges.push(VisualBinRangeHelper.GetVisualBinRange(binRanges[1], this.HistoOp.Result, this.HistoOp.Y, this.ChartType)); + this.VisualBinRanges.push(VisualBinRangeHelper.GetVisualBinRange(binRanges[0], this.HistoOp!.Result!, this.HistoOp!.X, this.ChartType)); + this.VisualBinRanges.push(VisualBinRangeHelper.GetVisualBinRange(binRanges[1], this.HistoOp!.Result!, this.HistoOp!.Y, this.ChartType)); if (!this.HistoOp.Result.isEmpty) { this.MaxValue = Number.MIN_VALUE; @@ -79,11 +79,6 @@ export class HistogramBox extends React.Component { ); } - @computed - get createOperationParamsCache() { - return this.HistoOp!.CreateOperationParameters(); - } - activateHistogramOperation() { this.props.doc.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt) => { if (histoOp) { @@ -91,7 +86,7 @@ export class HistogramBox extends React.Component { this.HistoOp!.Update(); reaction( () => this.createOperationParamsCache, - () => this.HistoOp!.Update(); + () => this.HistoOp!.Update()); reaction(() => this.props.doc.GetList(KeyStore.LinkedFromDocs, []), (docs: Document[]) => { this.HistoOp!.Links.length = 0; @@ -107,22 +102,16 @@ export class HistogramBox extends React.Component { return
; } - drawRect(r: PIXIRectangle, color: number, tapHandler: () => void) { - return
- } - private renderGridLinesAndLabels(axis: number) { let sc = this.SizeConverter!; - let labels = this.VisualBinRanges[axis].GetLabels(); - - let dim = sc.RenderSize[axis] / sc.MaxLabelSizes[axis].coords[axis] + 5; - let mod = Math.ceil(labels.length / dim); + if (!sc || !this.VisualBinRanges.length) + return (null); + let dim = sc.RenderSize[axis] / ((axis == 0 && this.VisualBinRanges[axis] instanceof NominalVisualBinRange) ? + (12 + 5) : // (FontStyles.AxisLabel.fontSize + 5))); + sc.MaxLabelSizes[axis].coords[axis] + 5); - if (axis == 0 && this.VisualBinRanges[axis] instanceof NominalVisualBinRange) { - mod = Math.ceil( - labels.length / (sc.RenderSize[0] / (12 + 5))); // (FontStyles.AxisLabel.fontSize + 5))); - } let prims: JSX.Element[] = []; + let labels = this.VisualBinRanges[axis].GetLabels(); labels.map((binLabel, i) => { let xFrom = sc.DataToScreenX(axis === 0 ? binLabel.minValue! : sc.DataMins[0]); let xTo = sc.DataToScreenX(axis === 0 ? binLabel.maxValue! : sc.DataMaxs[0]); @@ -133,7 +122,7 @@ export class HistogramBox extends React.Component { if (i == labels.length - 1) prims.push(this.drawLine(axis == 0 ? xTo : xFrom, axis == 0 ? yFrom : yTo, axis == 0 ? 1 : xTo - xFrom, axis == 0 ? yTo - yFrom : 1)); - if (i % mod === 0 && binLabel.label) { + if (i % Math.ceil(labels.length / dim) === 0 && binLabel.label) { let text = binLabel.label; if (text.length >= StyleConstants.MAX_CHAR_FOR_HISTOGRAM_LABELS) { text = text.slice(0, StyleConstants.MAX_CHAR_FOR_HISTOGRAM_LABELS - 3) + "..."; @@ -157,330 +146,21 @@ export class HistogramBox extends React.Component { return prims; } - @computed - get binPrimitives() { - if (!this.HistoOp || !(this.HistoOp.Result instanceof HistogramResult) || !this.SizeConverter) - return undefined; - let prims: JSX.Element[] = []; - let selectedBinPrimitiveCollections = new Array(); - let allBrushIndex = ModelHelpers.AllBrushIndex(this.HistoOp.Result); - for (let key in this.HistoOp.Result.bins) { - if (this.HistoOp.Result.bins.hasOwnProperty(key)) { - let drawPrims = new HistogramBinPrimitiveCollection(key, this); - - let filterModel = ModelHelpers.GetBinFilterModel(this.HistoOp.Result.bins![key], allBrushIndex, this.HistoOp.Result, this.HistoOp.X, this.HistoOp.Y); - - this.HitTargets.setValue(drawPrims.HitGeom, filterModel); - - if (ArrayUtil.Contains(this.HistoOp.FilterModels, filterModel)) { - selectedBinPrimitiveCollections.push(drawPrims); - } - - drawPrims.BinPrimitives.filter(bp => bp.DataValue && bp.BrushIndex !== allBrushIndex).map(binPrimitive => { - let toggleFilter = () => { - if ([filterModel].filter(h => ArrayUtil.Contains(this.HistoOp!.FilterModels, h)).length > 0) - this.HistoOp!.RemoveFilterModels([filterModel]); - else this.HistoOp!.AddFilterModels([filterModel]); - } - prims.push(this.drawRect(binPrimitive.Rect, binPrimitive.Color, () => runInAction(toggleFilter))); - prims.push(this.drawRect(binPrimitive.MarginRect, StyleConstants.MARGIN_BARS_COLOR, () => runInAction(toggleFilter))); - }); - } - } - return prims; - } - render() { - trace(); - if (!this.binPrimitives || !this.VisualBinRanges.length) { - return (null); - } - + let label = this.HistoOp && this.HistoOp.X ? this.HistoOp.X.AttributeModel.DisplayName : "<...>"; + let xaxislines = this.xaxislines; + let yaxislines = this.yaxislines; return ( runInAction(() => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height })}> {({ measureRef }) =>
- {this.xaxislines} - {this.yaxislines} - {this.binPrimitives} -
{this.HistoOp!.X.AttributeModel.DisplayName}
+ {xaxislines} + {yaxislines} + +
{label}
}
) } -} - -export class HistogramBinPrimitive { - constructor(init?: Partial) { - 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; -} - -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; } - public BinPrimitives: Array = new Array(); - public HitGeom: PIXIRectangle = PIXIRectangle.EMPTY; - - constructor(key: string, histoBox: HistogramBox) { - this._histoBox = histoBox; - let bin = this.histoResult.bins![key]; - - var overlapBrushIndex = ModelHelpers.OverlapBrushIndex(this.histoResult); - var orderedBrushes = new Array(); - orderedBrushes.push(this.histoResult.brushes![0]); - orderedBrushes.push(this.histoResult.brushes![overlapBrushIndex]); - for (var b = 0; b < this.histoResult.brushes!.length; b++) { - var brush = this.histoResult.brushes![b]; - if (brush.brushIndex != 0 && brush.brushIndex != overlapBrushIndex) { - orderedBrushes.push(brush); - } - } - var binBrushMaxAxis = this.getBinBrushAxisRange(bin, orderedBrushes, this.histoOp.Normalization); // X= 0, Y = 1 - - var brushFactorSum: number = 0; - for (var b = 0; b < orderedBrushes.length; b++) { - var brush = orderedBrushes[b]; - var valueAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.V, this.histoResult, brush.brushIndex!); - var doubleRes = ModelHelpers.GetAggregateResult(bin, valueAggregateKey) as DoubleValueAggregateResult; - var unNormalizedValue = (doubleRes != null && doubleRes.hasResult) ? doubleRes.result : null; - if (unNormalizedValue) - switch (histoBox.ChartType) { - case ChartType.VerticalBar: - this.createVerticalBarChartBinPrimitives(bin, brush, binBrushMaxAxis, this.histoOp.Normalization, histoBox.SizeConverter!); // X = 0, Y = 1, NOne = -1 - break; - case ChartType.HorizontalBar: - this.createHorizontalBarChartBinPrimitives(bin, brush, binBrushMaxAxis, this.histoOp.Normalization, histoBox.SizeConverter!); - break; - case ChartType.SinglePoint: - this.createSinlgePointChartBinPrimitives(bin, brush, unNormalizedValue, histoBox.SizeConverter!); - break; - case ChartType.HeatMap: - var normalizedValue = (unNormalizedValue - histoBox.MinValue) / (Math.abs((histoBox.MaxValue - histoBox.MinValue)) < HistogramBinPrimitiveCollection.TOLERANCE ? - unNormalizedValue : histoBox.MaxValue - histoBox.MinValue); - brushFactorSum = this.createHeatmapBinPrimitives(bin, brush, unNormalizedValue, brushFactorSum, normalizedValue, histoBox.SizeConverter!); - } - } - - // adjust brush rects (stacking or not) - var sum: number = 0; - var allBrushIndex = ModelHelpers.AllBrushIndex(this.histoResult); - var filteredBinPrims = this.BinPrimitives.filter(b => b.BrushIndex != allBrushIndex && b.DataValue != 0.0); - var count: number = filteredBinPrims.length; - filteredBinPrims.map(fbp => { - if (histoBox.ChartType == ChartType.VerticalBar) { - if (this.histoOp.X.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); - 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 / count, 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); - 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); - 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 / count); - fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x, fbp.MarginRect.y - h + sum + (fbp.Rect.height / 2.0), fbp.MarginRect.width, fbp.MarginRect.height); - sum += fbp.Rect.height; - } - } - }); - 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 getBinBrushAxisRange(bin: Bin, brushes: Array, axis: number): number { - var binBrushMaxAxis = Number.MIN_VALUE; - brushes.forEach((Brush) => { - var maxAggregateKey = ModelHelpers.CreateAggregateKey(axis === 0 ? this.histoOp.Y : this.histoOp.X, this.histoResult, Brush.brushIndex!); - var aggResult = ModelHelpers.GetAggregateResult(bin, maxAggregateKey) as DoubleValueAggregateResult; - if (aggResult != null) { - if (aggResult.result! > binBrushMaxAxis) - binBrushMaxAxis = aggResult.result!; - } - }); - return binBrushMaxAxis; - } - private createHeatmapBinPrimitives(bin: Bin, brush: Brush, unNormalizedValue: number, brushFactorSum: number, normalizedValue: number, sizeConverter: SizeConverter): number { - - var valueAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.V, this.histoResult, ModelHelpers.AllBrushIndex(this.histoResult)); - var allUnNormalizedValue = ModelHelpers.GetAggregateResult(bin, valueAggregateKey) as DoubleValueAggregateResult; - - var tx = this._histoBox.VisualBinRanges[0].GetValueFromIndex(bin.binIndex!.indices![0]); - var xFrom = sizeConverter.DataToScreenX(tx); - var xTo = sizeConverter.DataToScreenX(this._histoBox.VisualBinRanges[0].AddStep(tx)); - - var ty = this._histoBox.VisualBinRanges[1].GetValueFromIndex(bin.binIndex!.indices![1]); - var yFrom = sizeConverter.DataToScreenY(ty); - var yTo = sizeConverter.DataToScreenY(this._histoBox.VisualBinRanges[1].AddStep(ty)); - - var returnBrushFactorSum = brushFactorSum; - if (allUnNormalizedValue.hasResult) { - var brushFactor = (unNormalizedValue / allUnNormalizedValue.result!); - 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(bin, brush, PIXIRectangle.EMPTY, 0, xFrom, xTo, yFrom, yTo, dataColor, 1, unNormalizedValue); - return returnBrushFactorSum; - } - - private createSinlgePointChartBinPrimitives(bin: Bin, brush: Brush, unNormalizedValue: number, sizeConverter: SizeConverter): void { - var yAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Y, this.histoResult, brush.brushIndex!); - var xAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.X, this.histoResult, brush.brushIndex!); - - var xValue = ModelHelpers.GetAggregateResult(bin, xAggregateKey) as DoubleValueAggregateResult; - if (!xValue.hasResult) - return; - var xFrom = sizeConverter.DataToScreenX(xValue.result!) - 5; - var xTo = sizeConverter.DataToScreenX(xValue.result!) + 5; - - var yValue = ModelHelpers.GetAggregateResult(bin, yAggregateKey) as DoubleValueAggregateResult;; - if (!yValue.hasResult) - return; - var yFrom = sizeConverter.DataToScreenY(yValue.result!) + 5; - var yTo = sizeConverter.DataToScreenY(yValue.result!); - - this.createBinPrimitive(bin, brush, PIXIRectangle.EMPTY, 0, xFrom, xTo, yFrom, yTo, this.baseColorFromBrush(brush), 1, unNormalizedValue); - } - - private createVerticalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number, sizeConverter: SizeConverter): void { - var yAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Y, this.histoResult, brush.brushIndex!); - var marginParams = new MarginAggregateParameters(); - marginParams.aggregateFunction = this.histoOp.Y.AggregateFunction; - var yMarginAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Y, this.histoResult, - brush.brushIndex!, marginParams); - var dataValue = ModelHelpers.GetAggregateResult(bin, yAggregateKey) as DoubleValueAggregateResult; - - if (dataValue != null && dataValue.hasResult) { - var yValue = normalization != 0 || binBrushMaxAxis == 0 ? dataValue.result! : (dataValue.result! - 0) / (binBrushMaxAxis - 0) * sizeConverter.DataRanges[1]; - - var yFrom = sizeConverter.DataToScreenY(Math.min(0, yValue)); - var yTo = sizeConverter.DataToScreenY(Math.max(0, yValue));; - - var xValue = this._histoBox.VisualBinRanges[0].GetValueFromIndex(bin.binIndex!.indices![0])!; - var xFrom = sizeConverter.DataToScreenX(xValue); - var xTo = sizeConverter.DataToScreenX(this._histoBox.VisualBinRanges[0].AddStep(xValue)); - - var marginResult = ModelHelpers.GetAggregateResult(bin, yMarginAggregateKey) as MarginAggregateResult; - var yMarginAbsolute = !marginResult ? 0 : marginResult.absolutMargin!; - var marginRect = new PIXIRectangle(xFrom + (xTo - xFrom) / 2.0 - 1, - sizeConverter.DataToScreenY(yValue + yMarginAbsolute), 2, - sizeConverter.DataToScreenY(yValue - yMarginAbsolute) - sizeConverter.DataToScreenY(yValue + yMarginAbsolute)); - - this.createBinPrimitive(bin, brush, marginRect, 0, xFrom, xTo, yFrom, yTo, - this.baseColorFromBrush(brush), normalization != 0 ? 1 : 0.6 * binBrushMaxAxis / sizeConverter.DataRanges[1] + 0.4, dataValue.result!); - } - } - - private createHorizontalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number, sizeConverter: SizeConverter): void { - var xAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.X, this.histoResult, brush.brushIndex!); - var marginParams = new MarginAggregateParameters(); - marginParams.aggregateFunction = this.histoOp.X.AggregateFunction; - var xMarginAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.X, this.histoResult, - brush.brushIndex!, marginParams); - var dataValue = ModelHelpers.GetAggregateResult(bin, xAggregateKey) as DoubleValueAggregateResult; - - if (dataValue != null && dataValue.hasResult) { - var xValue = normalization != 1 || binBrushMaxAxis == 0 ? dataValue.result! : (dataValue.result! - 0) / (binBrushMaxAxis - 0) * sizeConverter.DataRanges[0]; - var xFrom = sizeConverter.DataToScreenX(Math.min(0, xValue)); - var xTo = sizeConverter.DataToScreenX(Math.max(0, xValue)); - - var yValue = this._histoBox.VisualBinRanges[1].GetValueFromIndex(bin.binIndex!.indices![1]); - var yFrom = yValue; - var yTo = this._histoBox.VisualBinRanges[1].AddStep(yValue); - - var marginResult = ModelHelpers.GetAggregateResult(bin, xMarginAggregateKey) as MarginAggregateResult; - var xMarginAbsolute = sizeConverter.IsSmall || !marginResult ? 0 : marginResult.absolutMargin!; - - var marginRect = new PIXIRectangle(sizeConverter.DataToScreenX(xValue - xMarginAbsolute), - yTo + (yFrom - yTo) / 2.0 - 1, - sizeConverter.DataToScreenX(xValue + xMarginAbsolute) - sizeConverter.DataToScreenX(xValue - xMarginAbsolute), - 2.0); - - this.createBinPrimitive(bin, brush, marginRect, 0, xFrom, xTo, yFrom, yTo, - this.baseColorFromBrush(brush), normalization != 1 ? 1 : 0.6 * binBrushMaxAxis / sizeConverter.DataRanges[0] + 0.4, dataValue.result!); - } - } - - private createBinPrimitive(bin: Bin, brush: Brush, marginRect: PIXIRectangle, - marginPercentage: number, xFrom: number, xTo: number, yFrom: number, yTo: number, color: number, opacity: number, dataValue: number) { - // hitgeom todo - - 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 - }); - this.BinPrimitives.push(binPrimitive); - } - - private baseColorFromBrush(brush: Brush): number { - var baseColor: number = StyleConstants.HIGHLIGHT_COLOR; - if (brush.brushIndex == ModelHelpers.RestBrushIndex(this.histoResult)) { - baseColor = StyleConstants.HIGHLIGHT_COLOR; - } - else if (brush.brushIndex == ModelHelpers.OverlapBrushIndex(this.histoResult)) { - baseColor = StyleConstants.OVERLAP_COLOR; - } - else if (brush.brushIndex == ModelHelpers.AllBrushIndex(this.histoResult)) { - baseColor = 0x00ff00; - } - else { - if (this._histoBox.HistoOp!.BrushColors.length > 0) { - baseColor = this._histoBox.HistoOp!.BrushColors[brush.brushIndex! % this._histoBox.HistoOp!.BrushColors.length]; - } - else { - baseColor = StyleConstants.HIGHLIGHT_COLOR; - } - } - return baseColor; - } } \ No newline at end of file diff --git a/src/client/views/nodes/HistogramBoxPrimitives.tsx b/src/client/views/nodes/HistogramBoxPrimitives.tsx new file mode 100644 index 000000000..2f4a553b7 --- /dev/null +++ b/src/client/views/nodes/HistogramBoxPrimitives.tsx @@ -0,0 +1,373 @@ +import React = require("react") +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 { SizeConverter } from "../../northstar/utils/SizeConverter"; +import { StyleConstants } from "../../northstar/utils/StyleContants"; +import "./HistogramBox.scss"; +import { HistogramBox } from "./HistogramBox"; +import { computed, runInAction, observable, trace } from "mobx"; +import { ArrayUtil } from "../../northstar/utils/ArrayUtil"; +import { Utils as DashUtils } from '../../../Utils'; +import { observer } from "mobx-react"; + + +export interface HistogramBoxPrimitivesProps { + HistoBox: HistogramBox; +} + +@observer +export class HistogramBoxPrimitives extends React.Component { + @observable _selectedPrims: HistogramBinPrimitive[] = []; + + @computed + get selectedPrimitives() { + return this._selectedPrims.map((bp) => this.drawBorder(bp.Rect, StyleConstants.OPERATOR_BACKGROUND_COLOR)); + } + @computed + get binPrimitives() { + if (!this.props.HistoBox.HistoOp || !(this.props.HistoBox.HistoOp.Result instanceof HistogramResult) || !this.props.HistoBox.SizeConverter) + return (null); + let prims: JSX.Element[] = []; + let allBrushIndex = ModelHelpers.AllBrushIndex(this.props.HistoBox.HistoOp.Result); + for (let key in this.props.HistoBox.HistoOp.Result.bins) { + if (this.props.HistoBox.HistoOp.Result.bins.hasOwnProperty(key)) { + let drawPrims = new HistogramBinPrimitiveCollection(key, this.props.HistoBox); + let filterModel = ModelHelpers.GetBinFilterModel(this.props.HistoBox.HistoOp.Result.bins![key], allBrushIndex, this.props.HistoBox.HistoOp.Result, this.props.HistoBox.HistoOp.X, this.props.HistoBox.HistoOp.Y); + + this.props.HistoBox.HitTargets.setValue(drawPrims.HitGeom, filterModel); + + drawPrims.BinPrimitives.filter(bp => bp.DataValue && bp.BrushIndex !== allBrushIndex).map(binPrimitive => { + let toggleFilter = () => { + if ([filterModel].filter(h => ArrayUtil.Contains(this.props.HistoBox.HistoOp!.FilterModels, h)).length > 0) { + let bp = ArrayUtil.FirstOrDefault(drawPrims.BinPrimitives, (bp: HistogramBinPrimitive) => bp.BrushIndex == allBrushIndex); + if (bp && bp.DataValue) { + this._selectedPrims.splice(this._selectedPrims.indexOf(bp), 1); + } + this.props.HistoBox.HistoOp!.RemoveFilterModels([filterModel]); + } + else { + let bp = ArrayUtil.FirstOrDefault(drawPrims.BinPrimitives, (bp: HistogramBinPrimitive) => bp.BrushIndex == allBrushIndex); + if (bp && bp.DataValue) { + this._selectedPrims.push(bp!); + } + this.props.HistoBox.HistoOp!.AddFilterModels([filterModel]); + } + } + prims.push(this.drawRect(binPrimitive.Rect, binPrimitive.Color, () => runInAction(toggleFilter))); + prims.push(this.drawRect(binPrimitive.MarginRect, StyleConstants.MARGIN_BARS_COLOR, () => runInAction(toggleFilter))); + }); + } + } + return prims; + } + drawBorder(r: PIXIRectangle, color: number) { + return
+ } + + drawRect(r: PIXIRectangle, color: number, tapHandler: () => void) { + return
+ } + render() { + return
+ {this.binPrimitives} + {this.selectedPrimitives} +
+ } +} + + +class HistogramBinPrimitive { + constructor(init?: Partial) { + 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; +} + +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; } + public BinPrimitives: Array = new Array(); + public HitGeom: PIXIRectangle = PIXIRectangle.EMPTY; + + constructor(key: string, histoBox: HistogramBox) { + this._histoBox = histoBox; + let bin = this.histoResult.bins![key]; + + var overlapBrushIndex = ModelHelpers.OverlapBrushIndex(this.histoResult); + var orderedBrushes = new Array(); + orderedBrushes.push(this.histoResult.brushes![0]); + orderedBrushes.push(this.histoResult.brushes![overlapBrushIndex]); + for (var b = 0; b < this.histoResult.brushes!.length; b++) { + var brush = this.histoResult.brushes![b]; + if (brush.brushIndex != 0 && brush.brushIndex != overlapBrushIndex) { + orderedBrushes.push(brush); + } + } + var binBrushMaxAxis = this.getBinBrushAxisRange(bin, orderedBrushes, this.histoOp.Normalization); // X= 0, Y = 1 + + var brushFactorSum: number = 0; + for (var b = 0; b < orderedBrushes.length; b++) { + var brush = orderedBrushes[b]; + var valueAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.V, this.histoResult, brush.brushIndex!); + var doubleRes = ModelHelpers.GetAggregateResult(bin, valueAggregateKey) as DoubleValueAggregateResult; + var unNormalizedValue = (doubleRes != null && doubleRes.hasResult) ? doubleRes.result : null; + if (unNormalizedValue) + switch (histoBox.ChartType) { + case ChartType.VerticalBar: + this.createVerticalBarChartBinPrimitives(bin, brush, binBrushMaxAxis, this.histoOp.Normalization, histoBox.SizeConverter!); // X = 0, Y = 1, NOne = -1 + break; + case ChartType.HorizontalBar: + this.createHorizontalBarChartBinPrimitives(bin, brush, binBrushMaxAxis, this.histoOp.Normalization, histoBox.SizeConverter!); + break; + case ChartType.SinglePoint: + this.createSinlgePointChartBinPrimitives(bin, brush, unNormalizedValue, histoBox.SizeConverter!); + break; + case ChartType.HeatMap: + var normalizedValue = (unNormalizedValue - histoBox.MinValue) / (Math.abs((histoBox.MaxValue - histoBox.MinValue)) < HistogramBinPrimitiveCollection.TOLERANCE ? + unNormalizedValue : histoBox.MaxValue - histoBox.MinValue); + brushFactorSum = this.createHeatmapBinPrimitives(bin, brush, unNormalizedValue, brushFactorSum, normalizedValue, histoBox.SizeConverter!); + } + } + + // adjust brush rects (stacking or not) + var sum: number = 0; + var allBrushIndex = ModelHelpers.AllBrushIndex(this.histoResult); + var filteredBinPrims = this.BinPrimitives.filter(b => b.BrushIndex != allBrushIndex && b.DataValue != 0.0); + var count: number = filteredBinPrims.length; + filteredBinPrims.map(fbp => { + if (histoBox.ChartType == ChartType.VerticalBar) { + if (this.histoOp.X.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); + 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 / count, 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); + 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); + 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 / count); + fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x, fbp.MarginRect.y - h + sum + (fbp.Rect.height / 2.0), fbp.MarginRect.width, fbp.MarginRect.height); + sum += fbp.Rect.height; + } + } + }); + 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 getBinBrushAxisRange(bin: Bin, brushes: Array, axis: number): number { + var binBrushMaxAxis = Number.MIN_VALUE; + brushes.forEach((Brush) => { + var maxAggregateKey = ModelHelpers.CreateAggregateKey(axis === 0 ? this.histoOp.Y : this.histoOp.X, this.histoResult, Brush.brushIndex!); + var aggResult = ModelHelpers.GetAggregateResult(bin, maxAggregateKey) as DoubleValueAggregateResult; + if (aggResult != null) { + if (aggResult.result! > binBrushMaxAxis) + binBrushMaxAxis = aggResult.result!; + } + }); + return binBrushMaxAxis; + } + private createHeatmapBinPrimitives(bin: Bin, brush: Brush, unNormalizedValue: number, brushFactorSum: number, normalizedValue: number, sizeConverter: SizeConverter): number { + + var valueAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.V, this.histoResult, ModelHelpers.AllBrushIndex(this.histoResult)); + var allUnNormalizedValue = ModelHelpers.GetAggregateResult(bin, valueAggregateKey) as DoubleValueAggregateResult; + + var tx = this._histoBox.VisualBinRanges[0].GetValueFromIndex(bin.binIndex!.indices![0]); + var xFrom = sizeConverter.DataToScreenX(tx); + var xTo = sizeConverter.DataToScreenX(this._histoBox.VisualBinRanges[0].AddStep(tx)); + + var ty = this._histoBox.VisualBinRanges[1].GetValueFromIndex(bin.binIndex!.indices![1]); + var yFrom = sizeConverter.DataToScreenY(ty); + var yTo = sizeConverter.DataToScreenY(this._histoBox.VisualBinRanges[1].AddStep(ty)); + + var returnBrushFactorSum = brushFactorSum; + if (allUnNormalizedValue.hasResult) { + var brushFactor = (unNormalizedValue / allUnNormalizedValue.result!); + 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(bin, brush, PIXIRectangle.EMPTY, 0, xFrom, xTo, yFrom, yTo, dataColor, 1, unNormalizedValue); + return returnBrushFactorSum; + } + + private createSinlgePointChartBinPrimitives(bin: Bin, brush: Brush, unNormalizedValue: number, sizeConverter: SizeConverter): void { + var yAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Y, this.histoResult, brush.brushIndex!); + var xAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.X, this.histoResult, brush.brushIndex!); + + var xValue = ModelHelpers.GetAggregateResult(bin, xAggregateKey) as DoubleValueAggregateResult; + if (!xValue.hasResult) + return; + var xFrom = sizeConverter.DataToScreenX(xValue.result!) - 5; + var xTo = sizeConverter.DataToScreenX(xValue.result!) + 5; + + var yValue = ModelHelpers.GetAggregateResult(bin, yAggregateKey) as DoubleValueAggregateResult;; + if (!yValue.hasResult) + return; + var yFrom = sizeConverter.DataToScreenY(yValue.result!) + 5; + var yTo = sizeConverter.DataToScreenY(yValue.result!); + + this.createBinPrimitive(bin, brush, PIXIRectangle.EMPTY, 0, xFrom, xTo, yFrom, yTo, this.baseColorFromBrush(brush), 1, unNormalizedValue); + } + + private createVerticalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number, sizeConverter: SizeConverter): void { + var yAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Y, this.histoResult, brush.brushIndex!); + var marginParams = new MarginAggregateParameters(); + marginParams.aggregateFunction = this.histoOp.Y.AggregateFunction; + var yMarginAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Y, this.histoResult, + brush.brushIndex!, marginParams); + var dataValue = ModelHelpers.GetAggregateResult(bin, yAggregateKey) as DoubleValueAggregateResult; + + if (dataValue != null && dataValue.hasResult) { + var yValue = normalization != 0 || binBrushMaxAxis == 0 ? dataValue.result! : (dataValue.result! - 0) / (binBrushMaxAxis - 0) * sizeConverter.DataRanges[1]; + + var yFrom = sizeConverter.DataToScreenY(Math.min(0, yValue)); + var yTo = sizeConverter.DataToScreenY(Math.max(0, yValue));; + + var xValue = this._histoBox.VisualBinRanges[0].GetValueFromIndex(bin.binIndex!.indices![0])!; + var xFrom = sizeConverter.DataToScreenX(xValue); + var xTo = sizeConverter.DataToScreenX(this._histoBox.VisualBinRanges[0].AddStep(xValue)); + + var marginResult = ModelHelpers.GetAggregateResult(bin, yMarginAggregateKey) as MarginAggregateResult; + var yMarginAbsolute = !marginResult ? 0 : marginResult.absolutMargin!; + var marginRect = new PIXIRectangle(xFrom + (xTo - xFrom) / 2.0 - 1, + sizeConverter.DataToScreenY(yValue + yMarginAbsolute), 2, + sizeConverter.DataToScreenY(yValue - yMarginAbsolute) - sizeConverter.DataToScreenY(yValue + yMarginAbsolute)); + + this.createBinPrimitive(bin, brush, marginRect, 0, xFrom, xTo, yFrom, yTo, + this.baseColorFromBrush(brush), normalization != 0 ? 1 : 0.6 * binBrushMaxAxis / sizeConverter.DataRanges[1] + 0.4, dataValue.result!); + } + } + + private createHorizontalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number, sizeConverter: SizeConverter): void { + var xAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.X, this.histoResult, brush.brushIndex!); + var marginParams = new MarginAggregateParameters(); + marginParams.aggregateFunction = this.histoOp.X.AggregateFunction; + var xMarginAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.X, this.histoResult, + brush.brushIndex!, marginParams); + var dataValue = ModelHelpers.GetAggregateResult(bin, xAggregateKey) as DoubleValueAggregateResult; + + if (dataValue != null && dataValue.hasResult) { + var xValue = normalization != 1 || binBrushMaxAxis == 0 ? dataValue.result! : (dataValue.result! - 0) / (binBrushMaxAxis - 0) * sizeConverter.DataRanges[0]; + var xFrom = sizeConverter.DataToScreenX(Math.min(0, xValue)); + var xTo = sizeConverter.DataToScreenX(Math.max(0, xValue)); + + var yValue = this._histoBox.VisualBinRanges[1].GetValueFromIndex(bin.binIndex!.indices![1]); + var yFrom = yValue; + var yTo = this._histoBox.VisualBinRanges[1].AddStep(yValue); + + var marginResult = ModelHelpers.GetAggregateResult(bin, xMarginAggregateKey) as MarginAggregateResult; + var xMarginAbsolute = sizeConverter.IsSmall || !marginResult ? 0 : marginResult.absolutMargin!; + + var marginRect = new PIXIRectangle(sizeConverter.DataToScreenX(xValue - xMarginAbsolute), + yTo + (yFrom - yTo) / 2.0 - 1, + sizeConverter.DataToScreenX(xValue + xMarginAbsolute) - sizeConverter.DataToScreenX(xValue - xMarginAbsolute), + 2.0); + + this.createBinPrimitive(bin, brush, marginRect, 0, xFrom, xTo, yFrom, yTo, + this.baseColorFromBrush(brush), normalization != 1 ? 1 : 0.6 * binBrushMaxAxis / sizeConverter.DataRanges[0] + 0.4, dataValue.result!); + } + } + + private createBinPrimitive(bin: Bin, brush: Brush, marginRect: PIXIRectangle, + marginPercentage: number, xFrom: number, xTo: number, yFrom: number, yTo: number, color: number, opacity: number, dataValue: number) { + // hitgeom todo + + 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 + }); + this.BinPrimitives.push(binPrimitive); + } + + private baseColorFromBrush(brush: Brush): number { + var baseColor: number = StyleConstants.HIGHLIGHT_COLOR; + if (brush.brushIndex == ModelHelpers.RestBrushIndex(this.histoResult)) { + baseColor = StyleConstants.HIGHLIGHT_COLOR; + } + else if (brush.brushIndex == ModelHelpers.OverlapBrushIndex(this.histoResult)) { + baseColor = StyleConstants.OVERLAP_COLOR; + } + else if (brush.brushIndex == ModelHelpers.AllBrushIndex(this.histoResult)) { + baseColor = 0x00ff00; + } + else { + if (this._histoBox.HistoOp!.BrushColors.length > 0) { + baseColor = this._histoBox.HistoOp!.BrushColors[brush.brushIndex! % this._histoBox.HistoOp!.BrushColors.length]; + } + else { + baseColor = StyleConstants.HIGHLIGHT_COLOR; + } + } + return baseColor; + } +} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 92aaac7cd78b9287d7f6ee85e73b3cad1c42d80c Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 26 Mar 2019 16:44:53 -0400 Subject: fixed histos in docking pane and not selecting bars when dragging. --- src/client/views/collections/CollectionFreeFormView.tsx | 4 ++-- src/client/views/nodes/DocumentContentsView.tsx | 1 + src/client/views/nodes/HistogramBox.tsx | 4 +++- src/client/views/nodes/HistogramBoxPrimitives.tsx | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) (limited to 'src/client/views/nodes/DocumentContentsView.tsx') diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 53fe969fe..1eab3e475 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -279,13 +279,13 @@ export class CollectionFreeFormView extends CollectionViewBase { get backgroundView() { return !this.backgroundLayout ? (null) : ( false} select={() => { }} />); + layoutKey={KeyStore.BackgroundLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />); } @computed get overlayView() { return !this.overlayLayout ? (null) : ( false} select={() => { }} />); + layoutKey={KeyStore.OverlayLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />); } getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform()) diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 40990b76a..f48be2047 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -35,6 +35,7 @@ export class DocumentContentsView extends React.Component()); } @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array()); } + CreateBindings(): JsxBindings { let bindings: JsxBindings = { ...this.props, }; for (const key of this.layoutKeys) { diff --git a/src/client/views/nodes/HistogramBox.tsx b/src/client/views/nodes/HistogramBox.tsx index 20a022227..4d7922c1b 100644 --- a/src/client/views/nodes/HistogramBox.tsx +++ b/src/client/views/nodes/HistogramBox.tsx @@ -150,10 +150,12 @@ export class HistogramBox extends React.Component { let label = this.HistoOp && this.HistoOp.X ? this.HistoOp.X.AttributeModel.DisplayName : "<...>"; let xaxislines = this.xaxislines; let yaxislines = this.yaxislines; + 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); return ( runInAction(() => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height })}> {({ measureRef }) => -
+
{xaxislines} {yaxislines} diff --git a/src/client/views/nodes/HistogramBoxPrimitives.tsx b/src/client/views/nodes/HistogramBoxPrimitives.tsx index 2f4a553b7..a8ada99a4 100644 --- a/src/client/views/nodes/HistogramBoxPrimitives.tsx +++ b/src/client/views/nodes/HistogramBoxPrimitives.tsx @@ -79,7 +79,7 @@ export class HistogramBoxPrimitives extends React.Component void) { - return
{ if (e.button == 0) tapHandler() }} style={{ position: "absolute", transform: `translate(${r.x}px,${r.y}px)`, -- cgit v1.2.3-70-g09d2 From c3eea60fa586e8ff9cf59497c041044fd0143bb1 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Thu, 28 Mar 2019 21:03:17 -0400 Subject: organized collectionfreeformview. fixed issues with showing links --- src/client/util/DocumentManager.ts | 24 +- .../views/collections/CollectionFreeFormView.scss | 116 ----- .../views/collections/CollectionFreeFormView.tsx | 525 --------------------- src/client/views/collections/CollectionView.tsx | 7 +- src/client/views/collections/MarqueeView.scss | 8 - src/client/views/collections/MarqueeView.tsx | 175 ------- src/client/views/collections/PreviewCursor.scss | 18 - src/client/views/collections/PreviewCursor.tsx | 76 --- .../CollectionFreeFormLinkView.scss | 6 + .../CollectionFreeFormLinkView.tsx | 37 ++ .../CollectionFreeFormLinksView.scss | 7 + .../CollectionFreeFormLinksView.tsx | 56 +++ .../collectionFreeForm/CollectionFreeFormView.scss | 95 ++++ .../collectionFreeForm/CollectionFreeFormView.tsx | 416 ++++++++++++++++ .../collectionFreeForm/MarqueeView.scss | 8 + .../collections/collectionFreeForm/MarqueeView.tsx | 175 +++++++ .../collectionFreeForm/PreviewCursor.scss | 18 + .../collectionFreeForm/PreviewCursor.tsx | 76 +++ src/client/views/nodes/DocumentContentsView.tsx | 2 +- 19 files changed, 924 insertions(+), 921 deletions(-) delete mode 100644 src/client/views/collections/CollectionFreeFormView.scss delete mode 100644 src/client/views/collections/CollectionFreeFormView.tsx delete mode 100644 src/client/views/collections/MarqueeView.scss delete mode 100644 src/client/views/collections/MarqueeView.tsx delete mode 100644 src/client/views/collections/PreviewCursor.scss delete mode 100644 src/client/views/collections/PreviewCursor.tsx create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx create mode 100644 src/client/views/collections/collectionFreeForm/MarqueeView.scss create mode 100644 src/client/views/collections/collectionFreeForm/MarqueeView.tsx create mode 100644 src/client/views/collections/collectionFreeForm/PreviewCursor.scss create mode 100644 src/client/views/collections/collectionFreeForm/PreviewCursor.tsx (limited to 'src/client/views/nodes/DocumentContentsView.tsx') diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 28624dba8..bf59fbb43 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,10 +1,11 @@ import React = require('react') import { observer } from 'mobx-react'; -import { observable, action } from 'mobx'; +import { observable, action, computed } from 'mobx'; import { Document } from "../../fields/Document" import { DocumentView } from '../views/nodes/DocumentView'; import { KeyStore } from '../../fields/KeyStore'; import { FieldWaiting } from '../../fields/Field'; +import { ListField } from '../../fields/ListField'; export class DocumentManager { @@ -74,4 +75,25 @@ export class DocumentManager { return (toReturn); } + + @computed + public get LinkedDocumentViews() { + return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => { + let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField); + if (linksList && linksList != FieldWaiting && linksList.Data.length) { + pairs.push(...linksList.Data.reduce((pairs, link) => { + if (link instanceof Document) { + let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document); + if (linkToDoc && linkToDoc != FieldWaiting) { + DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { + pairs.push({ a: dv, b: docView1, l: link }) + }) + } + } + return pairs; + }, [] as { a: DocumentView, b: DocumentView, l: Document }[])); + } + return pairs; + }, [] as { a: DocumentView, b: DocumentView, l: Document }[]); + } } \ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss deleted file mode 100644 index 7012ce6b9..000000000 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ /dev/null @@ -1,116 +0,0 @@ -@import "../global_variables"; - -.collectionfreeformview-linkLine { - stroke: black; - stroke-width: 3; - transform: translate(10000px,10000px); - pointer-events: all; -} -.collectionfreeformview-svgCanvas{ - transform: translate(-10000px,-10000px); - position: absolute; - width: 20000px; - height: 20000px; - pointer-events: none; -} -.collectionfreeformview-container { - .collectionfreeformview > .jsx-parser { - position: absolute; - height: 100%; - width: 100%; - } - - .inking-canvas { - transform-origin: 50000px 50000px; - } - //nested freeform views - // .collectionfreeformview-container { - // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px), - // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px); - // background-size: 30px 30px; - // } - - box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw; - border: 0px solid $light-color-secondary; - border-radius: $border-radius; - box-sizing: border-box; - position: relative; - overflow: hidden; - top: 0; - 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 { - position: absolute; - height: 100%; - } - .formattedTextBox-cont { - background: $light-color-secondary; - } - - .inking-canvas { - transform-origin: 50000px 50000px; - } - - opacity: 0.99; - border: 0px solid transparent; - border-radius: $border-radius; - box-sizing: border-box; - position:relative; - overflow: hidden; - top: 0; - left: 0; - width: 100%; - height: 100%; - .collectionfreeformview { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - .inking-canvas { - transform-origin: 50000px 50000px; - } - .formattedTextBox-cont { - background:yellow; - } - } -} - -// selection border...? -.border { - border-style: solid; - box-sizing: border-box; - width: 98%; - height: 98%; - border-radius: $border-radius; -} - -//this is an animation for the blinking cursor! -@keyframes blink { - 0% { - opacity: 0; - } - 49% { - opacity: 0; - } - 50% { - opacity: 1; - } -} - -#prevCursor { - animation: blink 1s infinite; -} diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx deleted file mode 100644 index 44f4eadaa..000000000 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ /dev/null @@ -1,525 +0,0 @@ -import { action, computed, observable, reaction, runInAction, trace, untracked, IReactionDisposer } from "mobx"; -import { observer } from "mobx-react"; -import { Document } from "../../../fields/Document"; -import { FieldWaiting, Field, Opt } from "../../../fields/Field"; -import { KeyStore } from "../../../fields/KeyStore"; -import { ListField } from "../../../fields/ListField"; -import { TextField } from "../../../fields/TextField"; -import { DragManager } from "../../util/DragManager"; -import { Transform } from "../../util/Transform"; -import { undoBatch } from "../../util/UndoManager"; -import { InkingCanvas } from "../InkingCanvas"; -import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; -import { DocumentContentsView } from "../nodes/DocumentContentsView"; -import { DocumentViewProps, DocumentView } from "../nodes/DocumentView"; -import "./CollectionFreeFormView.scss"; -import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; -import { CollectionViewBase, CollectionViewProps } from "./CollectionViewBase"; -import { MarqueeView } from "./MarqueeView"; -import { PreviewCursor } from "./PreviewCursor"; -import React = require("react"); -import v5 = require("uuid/v5"); -import { DocumentManager } from "../../util/DocumentManager"; -import { Utils } from "../../../Utils"; -import { Server } from "../../Server"; -import { AverageAggregateParameters } from "../../northstar/model/idea/idea"; - -@observer -export class CollectionFreeFormView extends CollectionViewBase { - public _canvasRef = React.createRef(); - private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) - - public addLiveTextBox = (newBox: Document) => { - // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself - this._selectOnLoaded = newBox.Id; - //set text to be the typed key and get focus on text box - this.props.addDocument(newBox, false); - //remove cursor from screen - this.PreviewCursorVisible = false; - } - - public selectDocuments = (docs: Document[]) => { - this.props.CollectionView.SelectedDocs.length = 0; - docs.map(d => this.props.CollectionView.SelectedDocs.push(d.Id)); - } - - public getActiveDocuments = () => { - var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); - const lvalue = this.props.Document.GetT>(this.props.fieldKey, ListField); - let active: Document[] = []; - if (lvalue && lvalue != FieldWaiting) { - lvalue.Data.map(doc => { - var page = doc.GetNumber(KeyStore.Page, -1); - if (page == curPage || page == -1) { - active.push(doc); - } - }) - } - - return active; - } - - //determines whether the blinking cursor for indicating whether a text will be made on key down is visible - @observable public PreviewCursorVisible: boolean = false; - @observable public MarqueeVisible = false; - @observable public DownX: number = 0; - @observable public DownY: number = 0; - @observable private _lastX: number = 0; - @observable private _lastY: number = 0; - - @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) } - @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) } - @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); } - @computed get isAnnotationOverlay() { return this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? - @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } - @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } - @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); } - @computed get centeringShiftX() { return !this.props.Document.GetNumber(KeyStore.NativeWidth, 0) ? this.props.panelWidth() / 2 : 0; } // shift so pan position is at center of window for non-overlay collections - @computed get centeringShiftY() { return !this.props.Document.GetNumber(KeyStore.NativeHeight, 0) ? this.props.panelHeight() / 2 : 0; }// shift so pan position is at center of window for non-overlay collections - - @undoBatch - @action - drop = (e: Event, de: DragManager.DropEvent) => { - if (super.drop(e, de)) { - if (de.data instanceof DragManager.DocumentDragData) { - 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); - } - return true; - } - return false; - } - - - @action - cleanupInteractions = () => { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - this.MarqueeVisible = false; - } - - @action - onPointerDown = (e: React.PointerEvent): void => { - this.PreviewCursorVisible = false; - if ((e.button === 2 && this.props.active() && (!this.isAnnotationOverlay || this.zoomScaling != 1)) || e.button == 0) { - document.removeEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointerup", this.onPointerUp); - this._lastX = this.DownX = e.pageX; - this._lastY = this.DownY = e.pageY; - } - } - - @action - onPointerUp = (e: PointerEvent): void => { - e.stopPropagation(); - - if (Math.abs(this.DownX - e.clientX) < 4 && Math.abs(this.DownY - e.clientY) < 4) { - //show preview text cursor on tap - this.PreviewCursorVisible = true; - //select is not already selected - if (!this.props.isSelected()) { - this.props.select(false); - } - } - this.cleanupInteractions(); - } - - @action - onPointerMove = (e: PointerEvent): void => { - if (!e.cancelBubble && this.props.active()) { - if (e.buttons == 1 && !e.altKey && !e.metaKey) { - this.MarqueeVisible = true; - } - if (this.MarqueeVisible) { - e.stopPropagation(); - e.preventDefault(); - } - else if ((!this.isAnnotationOverlay || this.zoomScaling != 1) && !e.shiftKey) { - let x = this.props.Document.GetNumber(KeyStore.PanX, 0); - let y = this.props.Document.GetNumber(KeyStore.PanY, 0); - let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - this.SetPan(x - dx, y - dy); - this._lastX = e.pageX; - this._lastY = e.pageY; - e.stopPropagation(); - e.preventDefault(); - } - } - } - - @action - onPointerWheel = (e: React.WheelEvent): void => { - this.props.select(false); - e.stopPropagation(); - e.preventDefault(); - let coefficient = 1000; - - if (e.ctrlKey) { - var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); - const coefficient = 1000; - let deltaScale = (1 - (e.deltaY / coefficient)); - this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale); - this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale); - e.stopPropagation(); - e.preventDefault(); - } else { - // if (modes[e.deltaMode] == 'pixels') coefficient = 50; - // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height?? - let transform = this.getTransform(); - - let deltaScale = (1 - (e.deltaY / coefficient)); - if (deltaScale * this.zoomScaling < 1 && this.isAnnotationOverlay) - deltaScale = 1 / this.zoomScaling; - let [x, y] = transform.transformPoint(e.clientX, e.clientY); - - let localTransform = this.getLocalTransform() - localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y) - // console.log(localTransform) - - this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); - this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale); - } - } - - @action - private SetPan(panX: number, panY: number) { - var x1 = this.getLocalTransform().inverse().Scale; - const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX)); - const newPanY = Math.min((1 - 1 / x1) * this.nativeHeight, Math.max(0, panY)); - this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX); - this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY); - } - - @action - onDrop = (e: React.DragEvent): void => { - var pt = this.getTransform().transformPoint(e.pageX, e.pageY); - super.onDrop(e, { x: pt[0], y: pt[1] }); - } - - onDragOver = (): void => { - } - - @action - bringToFront(doc: Document) { - const { fieldKey: fieldKey, Document: Document } = this.props; - - const value: Document[] = Document.GetList(fieldKey, []).slice(); - value.sort((doc1, doc2) => { - if (doc1 === doc) { - return 1; - } - if (doc2 === doc) { - return -1; - } - return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0); - }).map((doc, index) => { - doc.SetNumber(KeyStore.ZIndex, index + 1) - }); - } - - @computed get backgroundLayout(): string | undefined { - let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField); - if (field && field !== FieldWaiting) { - return field.Data; - } - } - @computed get overlayLayout(): string | undefined { - let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField); - if (field && field !== FieldWaiting) { - return field.Data; - } - } - - focusDocument = (doc: Document) => { - let x = doc.GetNumber(KeyStore.X, 0) + doc.GetNumber(KeyStore.Width, 0) / 2; - let y = doc.GetNumber(KeyStore.Y, 0) + doc.GetNumber(KeyStore.Height, 0) / 2; - this.SetPan(x, y); - this.props.focus(this.props.Document); - } - - getDocumentViewProps(document: Document): DocumentViewProps { - return { - Document: document, - AddDocument: this.props.addDocument, - RemoveDocument: this.props.removeDocument, - ScreenToLocalTransform: this.getTransform, - isTopMost: false, - SelectOnLoad: document.Id == this._selectOnLoaded, - PanelWidth: document.Width, - PanelHeight: document.Height, - ContentScaling: this.noScaling, - ContainingCollectionView: this.props.CollectionView, - focus: this.focusDocument - } - } - - @computed - get views() { - var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); - const lvalue = this.props.Document.GetT>(this.props.fieldKey, ListField); - if (lvalue && lvalue != FieldWaiting) { - return lvalue.Data.map(doc => { - if (!doc) return null; - var page = doc.GetNumber(KeyStore.Page, 0); - return (page != curPage && page != 0) ? (null) : - (); - }) - } - return null; - } - - @computed - get backgroundView() { - return !this.backgroundLayout ? (null) : - ( false} select={() => { }} />); - } - @computed - get overlayView() { - return !this.overlayLayout ? (null) : - ( false} select={() => { }} />); - } - - getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform()) - getMarqueeTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH) - getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY); - noScaling = () => 1; - - //when focus is lost, this will remove the preview cursor - @action - onBlur = (): void => { - this.PreviewCursorVisible = false; - } - - private crosshairs?: HTMLCanvasElement; - drawCrosshairs = (backgroundColor: string) => { - if (this.crosshairs) { - let c = this.crosshairs; - let ctx = c.getContext('2d'); - if (ctx) { - ctx.fillStyle = backgroundColor; - ctx.fillRect(0, 0, 20, 20); - - ctx.fillStyle = "black"; - ctx.lineWidth = 0.5; - - ctx.beginPath(); - - ctx.moveTo(10, 0); - ctx.lineTo(10, 8); - - ctx.moveTo(10, 20); - ctx.lineTo(10, 12); - - ctx.moveTo(0, 10); - ctx.lineTo(8, 10); - - ctx.moveTo(20, 10); - ctx.lineTo(12, 10); - - ctx.stroke(); - - // ctx.font = "10px Arial"; - // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10); - } - } - } - - render() { - let [dx, dy] = [this.centeringShiftX, this.centeringShiftY]; - - const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); - const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); - // const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0) + this.centeringShiftX; - // const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0) + this.centeringShiftY; - // console.log("center:", this.getLocalTransform().transformPoint(this.centeringShiftX, this.centeringShiftY)); - - return ( -
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))} - onWheel={this.onPointerWheel} - onDrop={this.onDrop.bind(this)} - onDragOver={this.onDragOver} - onBlur={this.onBlur} - style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}// , zIndex: !this.props.isTopMost ? -1 : undefined }} - tabIndex={0} - ref={this.createDropTarget}> -
- {this.backgroundView} - - - {this.views} - - {super.getCursors().map(entry => { - if (entry.Data.length > 0) { - let id = entry.Data[0][0]; - let email = entry.Data[0][1]; - let point = entry.Data[1]; - this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22") - return ( -
- { if (el) this.crosshairs = el }} - width={20} - height={20} - style={{ - position: 'absolute', - width: "20px", - height: "20px", - opacity: 0.5, - borderRadius: "50%", - border: "2px solid black" - }} - /> -

{email[0].toUpperCase()}

-
- ); - } - })} -
- - {this.overlayView} - -
- ); - } -} - -@observer -export class LinksView extends React.Component { - @observable _connections: { a: Document, b: Document, l: Document }[] = []; - - private _reactionDisposer: Opt; - - componentDidMount() { - this._reactionDisposer = reaction(() => DocumentManager.Instance.DocumentViews.map(dv => [ - dv.props.Document.Get(KeyStore.AnnotationOn, false), - dv.props.Document.Get(KeyStore.LinkedFromDocs, false), - dv.props.Document.Get(KeyStore.LinkedToDocs, false) - ]), () => runInAction(() => this._connections = this.findPairs()), { fireImmediately: true }); - } - componentWillUnmount() { - if (this._reactionDisposer) { - this._reactionDisposer(); - } - this._reactionDisposer = undefined; - } - - filtered(views: DocumentView[]) { - return views.filter(sv => - sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document == this.props.Document - ); - } - - findPairs() { - return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => { - untracked(() => { - let srcViews = [dv]; - let srcAnnot = dv.props.Document.GetT(KeyStore.AnnotationOn, Document); - if (srcAnnot && srcAnnot != FieldWaiting && srcAnnot instanceof Document) { - srcViews = DocumentManager.Instance.getDocumentViews(srcAnnot.GetPrototype() as Document) - } - let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField); - if (linksList && linksList != FieldWaiting && linksList.Data.length) { - pairs.push(...linksList.Data.reduce((pairs, link) => { - if (link instanceof Document) { - let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document); - if (linkToDoc && linkToDoc != FieldWaiting) { - DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { - let targetViews = [docView1]; - let docAnnot = docView1.props.Document.GetT(KeyStore.AnnotationOn, Document); - if (docAnnot && docAnnot != FieldWaiting && docAnnot instanceof Document) { - targetViews = DocumentManager.Instance.getDocumentViews(docAnnot.GetPrototype() as Document) - } - this.filtered(targetViews).map(tv => this.filtered(srcViews).map(sv => - pairs.push({ a: sv.props.Document, b: tv.props.Document, l: link }))) - }) - } - } - return pairs; - }, [] as { a: Document, b: Document, l: Document }[])); - } - }) - return pairs; - }, [] as { a: Document, b: Document, l: Document }[]); - } - - onPointerDown(e: React.PointerEvent) { - let line = (e.nativeEvent as any).path[0]; - line.style.stroke = "red"; - Server.GetField(line.id, action((f: Opt) => { - if (f instanceof Document) { - console.log(f.Title); - } - })); - } - - @computed - get uniqueConnections() { - return this._connections.reduce((y, t) => { - if (!y.reduce((found, yi) => { - if (yi.a == t.a && yi.b == t.b) { - if (yi.l != t.l) - yi.n++; - return true; - } - return found; - }, false)) { - y.push({ a: t.a, b: t.b, l: t.l, n: 1 }); - } - return y - }, [] as { a: Document, b: Document, l: Document, n: number }[]); - } - - render() { - if (!this._connections.length) - return (null); - return - {this.uniqueConnections.map(c => { - let x1 = c.a.GetNumber(KeyStore.X, 0) + c.a.GetNumber(KeyStore.Width, 0) / 2; - let y1 = c.a.GetNumber(KeyStore.Y, 0) + c.a.GetNumber(KeyStore.Height, 0) / 2; - let x2 = c.b.GetNumber(KeyStore.X, 0) + c.b.GetNumber(KeyStore.Width, 0) / 2; - let y2 = c.b.GetNumber(KeyStore.Y, 0) + c.b.GetNumber(KeyStore.Height, 0) / 2; - return ( - - ) - })} - - } -} \ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index c804085df..a1498e0ae 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -7,7 +7,7 @@ import { ContextMenu } from "../ContextMenu"; import React = require("react"); import { KeyStore } from "../../../fields/KeyStore"; import { NumberField } from "../../../fields/NumberField"; -import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import { CollectionDockingView } from "./CollectionDockingView"; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionViewProps } from "./CollectionViewBase"; @@ -104,6 +104,11 @@ export class CollectionView extends React.Component { break; } } + doc.GetTAsync(KeyStore.AnnotationOn, Document).then((annotationOn) => { + if (annotationOn == props.Document) { + doc.Set(KeyStore.AnnotationOn, undefined, true); + } + }) if (index !== -1) { value.splice(index, 1) diff --git a/src/client/views/collections/MarqueeView.scss b/src/client/views/collections/MarqueeView.scss deleted file mode 100644 index 6d9a79344..000000000 --- a/src/client/views/collections/MarqueeView.scss +++ /dev/null @@ -1,8 +0,0 @@ - -.marqueeView { - border-style: dashed; - box-sizing: border-box; - position: absolute; - border-width: 1px; - border-color: black; -} \ No newline at end of file diff --git a/src/client/views/collections/MarqueeView.tsx b/src/client/views/collections/MarqueeView.tsx deleted file mode 100644 index b48ad9c64..000000000 --- a/src/client/views/collections/MarqueeView.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { action, IReactionDisposer, observable, reaction } from "mobx"; -import { observer } from "mobx-react"; -import { Document } from "../../../fields/Document"; -import { FieldWaiting, Opt } from "../../../fields/Field"; -import { KeyStore } from "../../../fields/KeyStore"; -import { Documents } from "../../documents/Documents"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Transform } from "../../util/Transform"; -import { CollectionFreeFormView } from "./CollectionFreeFormView"; -import "./MarqueeView.scss"; -import React = require("react"); -import { InkField, StrokeData } from "../../../fields/InkField"; -import { Utils } from "../../../Utils"; -import { InkingCanvas } from "../InkingCanvas"; - -interface MarqueeViewProps { - getMarqueeTransform: () => Transform; - getTransform: () => Transform; - container: CollectionFreeFormView; - addDocument: (doc: Document, allowDuplicates: false) => boolean; - activeDocuments: () => Document[]; - selectDocuments: (docs: Document[]) => void; - removeDocument: (doc: Document) => boolean; -} - -@observer -export class MarqueeView extends React.Component -{ - private _reactionDisposer: Opt; - - @observable _lastX: number = 0; - @observable _lastY: number = 0; - @observable _downX: number = 0; - @observable _downY: number = 0; - - componentDidMount() { - this._reactionDisposer = reaction( - () => this.props.container.MarqueeVisible, - (visible: boolean) => this.onPointerDown(visible, this.props.container.DownX, this.props.container.DownY)) - } - componentWillUnmount() { - if (this._reactionDisposer) { - this._reactionDisposer(); - } - this.cleanupInteractions(); - } - - @action - cleanupInteractions = () => { - document.removeEventListener("pointermove", this.onPointerMove, true) - document.removeEventListener("pointerup", this.onPointerUp, true); - document.removeEventListener("keydown", this.marqueeCommand, true); - } - - @action - onPointerDown = (visible: boolean, downX: number, downY: number): void => { - if (visible) { - this._downX = this._lastX = downX; - this._downY = this._lastY = downY; - document.addEventListener("pointermove", this.onPointerMove, true) - document.addEventListener("pointerup", this.onPointerUp, true); - document.addEventListener("keydown", this.marqueeCommand, true); - } - } - - @action - onPointerMove = (e: PointerEvent): void => { - this._lastX = e.pageX; - this._lastY = e.pageY; - } - - @action - onPointerUp = (e: PointerEvent): void => { - this.cleanupInteractions(); - if (!e.shiftKey) { - SelectionManager.DeselectAll(); - } - this.props.selectDocuments(this.marqueeSelect()); - } - - intersectRect(r1: { left: number, top: number, width: number, height: number }, - r2: { left: number, top: number, width: number, height: number }) { - return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); - } - - get Bounds() { - let left = this._downX < this._lastX ? this._downX : this._lastX; - let top = this._downY < this._lastY ? this._downY : this._lastY; - let topLeft = this.props.getTransform().transformPoint(left, top); - let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) } - } - - @action - marqueeCommand = (e: KeyboardEvent) => { - if (e.key == "Backspace" || e.key == "Delete") { - this.marqueeSelect().map(d => this.props.removeDocument(d)); - this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField); - this.cleanupInteractions(); - } - if (e.key == "c") { - let bounds = this.Bounds; - let selected = this.marqueeSelect().map(d => { - this.props.removeDocument(d); - d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2); - d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2); - d.SetNumber(KeyStore.Page, 0); - d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0)); - return d; - }); - let liftedInk = this.marqueeInkSelect(true); - this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField); - //setTimeout(() => { - let newCollection = Documents.FreeformDocument(selected, { - x: bounds.left, - y: bounds.top, - panx: 0, - pany: 0, - width: bounds.width, - height: bounds.height, - backgroundColor: "Transparent", - ink: liftedInk, - title: "a nested collection" - }); - this.props.addDocument(newCollection, false); - // }, 100); - this.cleanupInteractions(); - } - } - marqueeInkSelect(select: boolean) { - let selRect = this.Bounds; - let centerShiftX = 0 - (selRect.left + selRect.width / 2); // moves each point by the offset that shifts the selection's center to the origin. - let centerShiftY = 0 - (selRect.top + selRect.height / 2); - let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); - if (ink && ink != FieldWaiting && ink.Data) { - let idata = new Map(); - ink.Data.forEach((value: StrokeData, key: string, map: any) => { - let inside = InkingCanvas.IntersectStrokeRect(value, selRect); - if (inside && select) { - idata.set(key, - { - pathData: value.pathData.map(val => { return { x: val.x + centerShiftX, y: val.y + centerShiftY } }), - color: value.color, - width: value.width, - tool: value.tool, - page: -1 - }); - } else if (!inside && !select) { - idata.set(key, value); - } - }) - return idata; - } - } - - marqueeSelect() { - let selRect = this.Bounds; - let selection: Document[] = []; - this.props.activeDocuments().map(doc => { - var x = doc.GetNumber(KeyStore.X, 0); - var y = doc.GetNumber(KeyStore.Y, 0); - var w = doc.GetNumber(KeyStore.Width, 0); - var h = doc.GetNumber(KeyStore.Height, 0); - if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) - selection.push(doc) - }) - return selection; - } - - render() { - let p = this.props.getMarqueeTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); - let v = this.props.getMarqueeTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - return (!this.props.container.MarqueeVisible ? (null) :
); - } -} \ No newline at end of file diff --git a/src/client/views/collections/PreviewCursor.scss b/src/client/views/collections/PreviewCursor.scss deleted file mode 100644 index a797411f6..000000000 --- a/src/client/views/collections/PreviewCursor.scss +++ /dev/null @@ -1,18 +0,0 @@ - -.previewCursor { - color: black; - position: absolute; - transform-origin: left top; - pointer-events: none; -} - -//this is an animation for the blinking cursor! -@keyframes blink { - 0% {opacity: 0} - 49%{opacity: 0} - 50% {opacity: 1} -} - -#previewCursor { - animation: blink 1s infinite; -} \ No newline at end of file diff --git a/src/client/views/collections/PreviewCursor.tsx b/src/client/views/collections/PreviewCursor.tsx deleted file mode 100644 index 8ae59a83a..000000000 --- a/src/client/views/collections/PreviewCursor.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { action, IReactionDisposer, observable, reaction } from "mobx"; -import { observer } from "mobx-react"; -import { Document } from "../../../fields/Document"; -import { Opt } from "../../../fields/Field"; -import { Documents } from "../../documents/Documents"; -import { Transform } from "../../util/Transform"; -import { CollectionFreeFormView } from "./CollectionFreeFormView"; -import "./PreviewCursor.scss"; -import React = require("react"); - - -export interface PreviewCursorProps { - getTransform: () => Transform; - container: CollectionFreeFormView; - addLiveTextDocument: (doc: Document) => void; -} - -@observer -export class PreviewCursor extends React.Component { - private _reactionDisposer: Opt; - - @observable _lastX: number = 0; - @observable _lastY: number = 0; - - componentDidMount() { - this._reactionDisposer = reaction( - () => this.props.container.PreviewCursorVisible, - (visible: boolean) => this.onCursorPlaced(visible, this.props.container.DownX, this.props.container.DownY)) - } - componentWillUnmount() { - if (this._reactionDisposer) { - this._reactionDisposer(); - } - this.cleanupInteractions(); - } - - - @action - cleanupInteractions = () => { - document.removeEventListener("keypress", this.onKeyPress, false); - } - - @action - onCursorPlaced = (visible: boolean, downX: number, downY: number): void => { - if (visible) { - document.addEventListener("keypress", this.onKeyPress, false); - this._lastX = downX; - this._lastY = downY; - } else - this.cleanupInteractions(); - } - - @action - onKeyPress = (e: KeyboardEvent) => { - // Mixing events between React and Native is finicky. In FormattedTextBox, we set the - // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore - // the keyPress here. - //if not these keys, make a textbox if preview cursor is active! - if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { - //make textbox and add it to this collection - let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY); - let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" }); - this.props.addLiveTextDocument(newBox); - e.stopPropagation(); - } - } - - render() { - //get local position and place cursor there! - let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY); - return ( - !this.props.container.PreviewCursorVisible ? (null) : -
I
) - - } -} \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss new file mode 100644 index 000000000..3b2f79be1 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -0,0 +1,6 @@ +.collectionfreeformlinkview-linkLine { + stroke: black; + stroke-width: 3; + transform: translate(10000px,10000px); + pointer-events: all; +} \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx new file mode 100644 index 000000000..e84f0c5ad --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -0,0 +1,37 @@ +import { observer } from "mobx-react"; +import { Document } from "../../../../fields/Document"; +import { KeyStore } from "../../../../fields/KeyStore"; +import { Utils } from "../../../../Utils"; +import "./CollectionFreeFormLinkView.scss"; +import React = require("react"); +import v5 = require("uuid/v5"); + +export interface CollectionFreeFormLinkViewProps { + A: Document; + B: Document; + LinkDocs: Document[]; +} + +@observer +export class CollectionFreeFormLinkView extends React.Component { + + onPointerDown = (e: React.PointerEvent) => { + this.props.LinkDocs.map(l => + console.log("Link:" + l.Title)); + } + render() { + let l = this.props.LinkDocs; + let a = this.props.A; + let b = this.props.B; + let x1 = a.GetNumber(KeyStore.X, 0) + a.GetNumber(KeyStore.Width, 0) / 2; + let y1 = a.GetNumber(KeyStore.Y, 0) + a.GetNumber(KeyStore.Height, 0) / 2; + let x2 = b.GetNumber(KeyStore.X, 0) + b.GetNumber(KeyStore.Width, 0) / 2; + let y2 = b.GetNumber(KeyStore.Y, 0) + b.GetNumber(KeyStore.Height, 0) / 2; + return ( + + ) + } +} \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss new file mode 100644 index 000000000..9af0df7f5 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss @@ -0,0 +1,7 @@ +.collectionfreeformlinksview-svgCanvas{ + transform: translate(-10000px,-10000px); + position: absolute; + width: 20000px; + height: 20000px; + pointer-events: none; + } \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx new file mode 100644 index 000000000..4f28f43eb --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -0,0 +1,56 @@ +import { computed } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../../fields/Document"; +import { FieldWaiting } from "../../../../fields/Field"; +import { KeyStore } from "../../../../fields/KeyStore"; +import { Utils } from "../../../../Utils"; +import { DocumentManager } from "../../../util/DocumentManager"; +import { DocumentView } from "../../nodes/DocumentView"; +import { CollectionViewProps } from "../CollectionViewBase"; +import "./CollectionFreeFormLinksView.scss"; +import React = require("react"); +import v5 = require("uuid/v5"); +import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; + +@observer +export class CollectionFreeFormLinksView extends React.Component { + + documentAnchors(view: DocumentView) { + let equalViews = [view]; + let containerDoc = view.props.Document.GetT(KeyStore.AnnotationOn, Document); + if (containerDoc && containerDoc != FieldWaiting && containerDoc instanceof Document) { + equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.GetPrototype() as Document) + } + return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document == this.props.Document); + } + + @computed + get uniqueConnections() { + return DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { + let srcViews = this.documentAnchors(connection.a); + let targetViews = this.documentAnchors(connection.b); + let possiblePairs: { a: Document, b: Document, }[] = []; + srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document }))); + possiblePairs.map(possiblePair => { + if (!drawnPairs.reduce((found, drawnPair) => { + let match = (possiblePair.a == drawnPair.a && possiblePair.b == drawnPair.b); + if (match) { + if (!drawnPair.l.reduce((found, link) => found || link.Id == connection.l.Id, false)) + drawnPair.l.push(connection.l); + } + return match || found; + }, false)) { + drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] as Document[] }); + } + }) + return drawnPairs + }, [] as { a: Document, b: Document, l: Document[] }[]); + } + + render() { + return ( + + {this.uniqueConnections.map(c => )} + ); + } +} \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss new file mode 100644 index 000000000..9c5e98005 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -0,0 +1,95 @@ +@import "../../global_variables"; + +.collectionfreeformview-container { + .collectionfreeformview > .jsx-parser { + position: absolute; + height: 100%; + width: 100%; + } + + .inking-canvas { + transform-origin: 50000px 50000px; + } + //nested freeform views + // .collectionfreeformview-container { + // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px), + // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px); + // background-size: 30px 30px; + // } + + box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw; + border: 0px solid $light-color-secondary; + border-radius: $border-radius; + box-sizing: border-box; + position: relative; + overflow: hidden; + top: 0; + 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 { + position: absolute; + height: 100%; + } + .formattedTextBox-cont { + background: $light-color-secondary; + } + + .inking-canvas { + transform-origin: 50000px 50000px; + } + + opacity: 0.99; + border: 0px solid transparent; + border-radius: $border-radius; + box-sizing: border-box; + position:relative; + overflow: hidden; + top: 0; + left: 0; + width: 100%; + height: 100%; + .collectionfreeformview { + .formattedTextBox-cont { + background:yellow; + } + } +} + +// selection border...? +.border { + border-style: solid; + box-sizing: border-box; + width: 98%; + height: 98%; + border-radius: $border-radius; +} + +//this is an animation for the blinking cursor! +@keyframes blink { + 0% { + opacity: 0; + } + 49% { + opacity: 0; + } + 50% { + opacity: 1; + } +} + +#prevCursor { + animation: blink 1s infinite; +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx new file mode 100644 index 000000000..1f90b0d46 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -0,0 +1,416 @@ +import { action, computed, observable } from "mobx"; +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 { TextField } from "../../../../fields/TextField"; +import { DragManager } from "../../../util/DragManager"; +import { Transform } from "../../../util/Transform"; +import { undoBatch } from "../../../util/UndoManager"; +import { InkingCanvas } from "../../InkingCanvas"; +import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; +import { DocumentContentsView } from "../../nodes/DocumentContentsView"; +import { DocumentViewProps } from "../../nodes/DocumentView"; +import { COLLECTION_BORDER_WIDTH } from "../CollectionView"; +import { CollectionViewBase } from "../CollectionViewBase"; +import { MarqueeView } from "./MarqueeView"; +import { PreviewCursor } from "./PreviewCursor"; +import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; +import "./CollectionFreeFormView.scss"; +import React = require("react"); +import v5 = require("uuid/v5"); + +@observer +export class CollectionFreeFormView extends CollectionViewBase { + public _canvasRef = React.createRef(); + private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) + + public addLiveTextBox = (newBox: Document) => { + // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself + this._selectOnLoaded = newBox.Id; + //set text to be the typed key and get focus on text box + this.props.addDocument(newBox, false); + //remove cursor from screen + this.PreviewCursorVisible = false; + } + + public selectDocuments = (docs: Document[]) => { + this.props.CollectionView.SelectedDocs.length = 0; + docs.map(d => this.props.CollectionView.SelectedDocs.push(d.Id)); + } + + public getActiveDocuments = () => { + var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); + const lvalue = this.props.Document.GetT>(this.props.fieldKey, ListField); + let active: Document[] = []; + if (lvalue && lvalue != FieldWaiting) { + lvalue.Data.map(doc => { + var page = doc.GetNumber(KeyStore.Page, -1); + if (page == curPage || page == -1) { + active.push(doc); + } + }) + } + + return active; + } + + //determines whether the blinking cursor for indicating whether a text will be made on key down is visible + @observable public PreviewCursorVisible: boolean = false; + @observable public MarqueeVisible = false; + @observable public DownX: number = 0; + @observable public DownY: number = 0; + @observable private _lastX: number = 0; + @observable private _lastY: number = 0; + + @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) } + @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) } + @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); } + @computed get isAnnotationOverlay() { return this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? + @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); } + @computed get centeringShiftX() { return !this.props.Document.GetNumber(KeyStore.NativeWidth, 0) ? this.props.panelWidth() / 2 : 0; } // shift so pan position is at center of window for non-overlay collections + @computed get centeringShiftY() { return !this.props.Document.GetNumber(KeyStore.NativeHeight, 0) ? this.props.panelHeight() / 2 : 0; }// shift so pan position is at center of window for non-overlay collections + + @undoBatch + @action + drop = (e: Event, de: DragManager.DropEvent) => { + if (super.drop(e, de)) { + if (de.data instanceof DragManager.DocumentDragData) { + 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); + } + return true; + } + return false; + } + + + @action + cleanupInteractions = () => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + this.MarqueeVisible = false; + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + this.PreviewCursorVisible = false; + if ((e.button === 2 && this.props.active() && (!this.isAnnotationOverlay || this.zoomScaling != 1)) || e.button == 0) { + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + this._lastX = this.DownX = e.pageX; + this._lastY = this.DownY = e.pageY; + } + } + + @action + onPointerUp = (e: PointerEvent): void => { + e.stopPropagation(); + + if (Math.abs(this.DownX - e.clientX) < 4 && Math.abs(this.DownY - e.clientY) < 4) { + //show preview text cursor on tap + this.PreviewCursorVisible = true; + //select is not already selected + if (!this.props.isSelected()) { + this.props.select(false); + } + } + this.cleanupInteractions(); + } + + @action + onPointerMove = (e: PointerEvent): void => { + if (!e.cancelBubble && this.props.active()) { + if (e.buttons == 1 && !e.altKey && !e.metaKey) { + this.MarqueeVisible = true; + } + if (this.MarqueeVisible) { + e.stopPropagation(); + e.preventDefault(); + } + else if ((!this.isAnnotationOverlay || this.zoomScaling != 1) && !e.shiftKey) { + let x = this.props.Document.GetNumber(KeyStore.PanX, 0); + let y = this.props.Document.GetNumber(KeyStore.PanY, 0); + let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + this.SetPan(x - dx, y - dy); + this._lastX = e.pageX; + this._lastY = e.pageY; + e.stopPropagation(); + e.preventDefault(); + } + } + } + + @action + onPointerWheel = (e: React.WheelEvent): void => { + this.props.select(false); + e.stopPropagation(); + e.preventDefault(); + let coefficient = 1000; + + if (e.ctrlKey) { + var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); + var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); + const coefficient = 1000; + let deltaScale = (1 - (e.deltaY / coefficient)); + this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale); + this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale); + e.stopPropagation(); + e.preventDefault(); + } else { + // if (modes[e.deltaMode] == 'pixels') coefficient = 50; + // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height?? + let transform = this.getTransform(); + + let deltaScale = (1 - (e.deltaY / coefficient)); + if (deltaScale * this.zoomScaling < 1 && this.isAnnotationOverlay) + deltaScale = 1 / this.zoomScaling; + let [x, y] = transform.transformPoint(e.clientX, e.clientY); + + let localTransform = this.getLocalTransform() + localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y) + // console.log(localTransform) + + this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); + this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale); + } + } + + @action + private SetPan(panX: number, panY: number) { + var x1 = this.getLocalTransform().inverse().Scale; + const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX)); + const newPanY = Math.min((1 - 1 / x1) * this.nativeHeight, Math.max(0, panY)); + this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX); + this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY); + } + + @action + onDrop = (e: React.DragEvent): void => { + var pt = this.getTransform().transformPoint(e.pageX, e.pageY); + super.onDrop(e, { x: pt[0], y: pt[1] }); + } + + onDragOver = (): void => { + } + + @action + bringToFront(doc: Document) { + const { fieldKey: fieldKey, Document: Document } = this.props; + + const value: Document[] = Document.GetList(fieldKey, []).slice(); + value.sort((doc1, doc2) => { + if (doc1 === doc) { + return 1; + } + if (doc2 === doc) { + return -1; + } + return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0); + }).map((doc, index) => { + doc.SetNumber(KeyStore.ZIndex, index + 1) + }); + } + + @computed get backgroundLayout(): string | undefined { + let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField); + if (field && field !== FieldWaiting) { + return field.Data; + } + } + @computed get overlayLayout(): string | undefined { + let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField); + if (field && field !== FieldWaiting) { + return field.Data; + } + } + + focusDocument = (doc: Document) => { + let x = doc.GetNumber(KeyStore.X, 0) + doc.GetNumber(KeyStore.Width, 0) / 2; + let y = doc.GetNumber(KeyStore.Y, 0) + doc.GetNumber(KeyStore.Height, 0) / 2; + this.SetPan(x, y); + this.props.focus(this.props.Document); + } + + getDocumentViewProps(document: Document): DocumentViewProps { + return { + Document: document, + AddDocument: this.props.addDocument, + RemoveDocument: this.props.removeDocument, + ScreenToLocalTransform: this.getTransform, + isTopMost: false, + SelectOnLoad: document.Id == this._selectOnLoaded, + PanelWidth: document.Width, + PanelHeight: document.Height, + ContentScaling: this.noScaling, + ContainingCollectionView: this.props.CollectionView, + focus: this.focusDocument + } + } + + @computed + get views() { + var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); + const lvalue = this.props.Document.GetT>(this.props.fieldKey, ListField); + if (lvalue && lvalue != FieldWaiting) { + return lvalue.Data.map(doc => { + if (!doc) return null; + var page = doc.GetNumber(KeyStore.Page, 0); + return (page != curPage && page != 0) ? (null) : + (); + }) + } + return null; + } + + @computed + get backgroundView() { + return !this.backgroundLayout ? (null) : + ( false} select={() => { }} />); + } + @computed + get overlayView() { + return !this.overlayLayout ? (null) : + ( false} select={() => { }} />); + } + + getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform()) + getMarqueeTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH) + getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY); + noScaling = () => 1; + + //when focus is lost, this will remove the preview cursor + @action + onBlur = (): void => { + this.PreviewCursorVisible = false; + } + + private crosshairs?: HTMLCanvasElement; + drawCrosshairs = (backgroundColor: string) => { + if (this.crosshairs) { + let c = this.crosshairs; + let ctx = c.getContext('2d'); + if (ctx) { + ctx.fillStyle = backgroundColor; + ctx.fillRect(0, 0, 20, 20); + + ctx.fillStyle = "black"; + ctx.lineWidth = 0.5; + + ctx.beginPath(); + + ctx.moveTo(10, 0); + ctx.lineTo(10, 8); + + ctx.moveTo(10, 20); + ctx.lineTo(10, 12); + + ctx.moveTo(0, 10); + ctx.lineTo(8, 10); + + ctx.moveTo(20, 10); + ctx.lineTo(12, 10); + + ctx.stroke(); + + // ctx.font = "10px Arial"; + // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10); + } + } + } + + render() { + let [dx, dy] = [this.centeringShiftX, this.centeringShiftY]; + + const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); + const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); + // const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0) + this.centeringShiftX; + // const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0) + this.centeringShiftY; + // console.log("center:", this.getLocalTransform().transformPoint(this.centeringShiftX, this.centeringShiftY)); + + return ( +
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))} + onWheel={this.onPointerWheel} + onDrop={this.onDrop.bind(this)} + onDragOver={this.onDragOver} + onBlur={this.onBlur} + style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}// , zIndex: !this.props.isTopMost ? -1 : undefined }} + tabIndex={0} + ref={this.createDropTarget}> +
+ {this.backgroundView} + + + {this.views} + + {super.getCursors().map(entry => { + if (entry.Data.length > 0) { + let id = entry.Data[0][0]; + let email = entry.Data[0][1]; + let point = entry.Data[1]; + this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22") + return ( +
+ { if (el) this.crosshairs = el }} + width={20} + height={20} + style={{ + position: 'absolute', + width: "20px", + height: "20px", + opacity: 0.5, + borderRadius: "50%", + border: "2px solid black" + }} + /> +

{email[0].toUpperCase()}

+
+ ); + } + })} +
+ + {this.overlayView} + +
+ ); + } +} \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss new file mode 100644 index 000000000..6d9a79344 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -0,0 +1,8 @@ + +.marqueeView { + border-style: dashed; + box-sizing: border-box; + position: absolute; + border-width: 1px; + border-color: black; +} \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx new file mode 100644 index 000000000..2692690dd --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -0,0 +1,175 @@ +import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../../fields/Document"; +import { FieldWaiting, Opt } from "../../../../fields/Field"; +import { KeyStore } from "../../../../fields/KeyStore"; +import { Documents } from "../../../documents/Documents"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { Transform } from "../../../util/Transform"; +import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import "./MarqueeView.scss"; +import React = require("react"); +import { InkField, StrokeData } from "../../../../fields/InkField"; +import { Utils } from "../../../../Utils"; +import { InkingCanvas } from "../../InkingCanvas"; + +interface MarqueeViewProps { + getMarqueeTransform: () => Transform; + getTransform: () => Transform; + container: CollectionFreeFormView; + addDocument: (doc: Document, allowDuplicates: false) => boolean; + activeDocuments: () => Document[]; + selectDocuments: (docs: Document[]) => void; + removeDocument: (doc: Document) => boolean; +} + +@observer +export class MarqueeView extends React.Component +{ + private _reactionDisposer: Opt; + + @observable _lastX: number = 0; + @observable _lastY: number = 0; + @observable _downX: number = 0; + @observable _downY: number = 0; + + componentDidMount() { + this._reactionDisposer = reaction( + () => this.props.container.MarqueeVisible, + (visible: boolean) => this.onPointerDown(visible, this.props.container.DownX, this.props.container.DownY)) + } + componentWillUnmount() { + if (this._reactionDisposer) { + this._reactionDisposer(); + } + this.cleanupInteractions(); + } + + @action + cleanupInteractions = () => { + document.removeEventListener("pointermove", this.onPointerMove, true) + document.removeEventListener("pointerup", this.onPointerUp, true); + document.removeEventListener("keydown", this.marqueeCommand, true); + } + + @action + onPointerDown = (visible: boolean, downX: number, downY: number): void => { + if (visible) { + this._downX = this._lastX = downX; + this._downY = this._lastY = downY; + document.addEventListener("pointermove", this.onPointerMove, true) + document.addEventListener("pointerup", this.onPointerUp, true); + document.addEventListener("keydown", this.marqueeCommand, true); + } + } + + @action + onPointerMove = (e: PointerEvent): void => { + this._lastX = e.pageX; + this._lastY = e.pageY; + } + + @action + onPointerUp = (e: PointerEvent): void => { + this.cleanupInteractions(); + if (!e.shiftKey) { + SelectionManager.DeselectAll(); + } + this.props.selectDocuments(this.marqueeSelect()); + } + + intersectRect(r1: { left: number, top: number, width: number, height: number }, + r2: { left: number, top: number, width: number, height: number }) { + return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); + } + + get Bounds() { + let left = this._downX < this._lastX ? this._downX : this._lastX; + let top = this._downY < this._lastY ? this._downY : this._lastY; + let topLeft = this.props.getTransform().transformPoint(left, top); + let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) } + } + + @action + marqueeCommand = (e: KeyboardEvent) => { + if (e.key == "Backspace" || e.key == "Delete") { + this.marqueeSelect().map(d => this.props.removeDocument(d)); + this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField); + this.cleanupInteractions(); + } + if (e.key == "c") { + let bounds = this.Bounds; + let selected = this.marqueeSelect().map(d => { + this.props.removeDocument(d); + d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2); + d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2); + d.SetNumber(KeyStore.Page, 0); + d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0)); + return d; + }); + let liftedInk = this.marqueeInkSelect(true); + this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField); + //setTimeout(() => { + let newCollection = Documents.FreeformDocument(selected, { + x: bounds.left, + y: bounds.top, + panx: 0, + pany: 0, + width: bounds.width, + height: bounds.height, + backgroundColor: "Transparent", + ink: liftedInk, + title: "a nested collection" + }); + this.props.addDocument(newCollection, false); + // }, 100); + this.cleanupInteractions(); + } + } + marqueeInkSelect(select: boolean) { + let selRect = this.Bounds; + let centerShiftX = 0 - (selRect.left + selRect.width / 2); // moves each point by the offset that shifts the selection's center to the origin. + let centerShiftY = 0 - (selRect.top + selRect.height / 2); + let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); + if (ink && ink != FieldWaiting && ink.Data) { + let idata = new Map(); + ink.Data.forEach((value: StrokeData, key: string, map: any) => { + let inside = InkingCanvas.IntersectStrokeRect(value, selRect); + if (inside && select) { + idata.set(key, + { + pathData: value.pathData.map(val => { return { x: val.x + centerShiftX, y: val.y + centerShiftY } }), + color: value.color, + width: value.width, + tool: value.tool, + page: -1 + }); + } else if (!inside && !select) { + idata.set(key, value); + } + }) + return idata; + } + } + + marqueeSelect() { + let selRect = this.Bounds; + let selection: Document[] = []; + this.props.activeDocuments().map(doc => { + var x = doc.GetNumber(KeyStore.X, 0); + var y = doc.GetNumber(KeyStore.Y, 0); + var w = doc.GetNumber(KeyStore.Width, 0); + var h = doc.GetNumber(KeyStore.Height, 0); + if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) + selection.push(doc) + }) + return selection; + } + + render() { + let p = this.props.getMarqueeTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); + let v = this.props.getMarqueeTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + return (!this.props.container.MarqueeVisible ? (null) :
); + } +} \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss new file mode 100644 index 000000000..a797411f6 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss @@ -0,0 +1,18 @@ + +.previewCursor { + color: black; + position: absolute; + transform-origin: left top; + pointer-events: none; +} + +//this is an animation for the blinking cursor! +@keyframes blink { + 0% {opacity: 0} + 49%{opacity: 0} + 50% {opacity: 1} +} + +#previewCursor { + animation: blink 1s infinite; +} \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx new file mode 100644 index 000000000..95364f04b --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx @@ -0,0 +1,76 @@ +import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../../fields/Document"; +import { Opt } from "../../../../fields/Field"; +import { Documents } from "../../../documents/Documents"; +import { Transform } from "../../../util/Transform"; +import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import "./PreviewCursor.scss"; +import React = require("react"); + + +export interface PreviewCursorProps { + getTransform: () => Transform; + container: CollectionFreeFormView; + addLiveTextDocument: (doc: Document) => void; +} + +@observer +export class PreviewCursor extends React.Component { + private _reactionDisposer: Opt; + + @observable _lastX: number = 0; + @observable _lastY: number = 0; + + componentDidMount() { + this._reactionDisposer = reaction( + () => this.props.container.PreviewCursorVisible, + (visible: boolean) => this.onCursorPlaced(visible, this.props.container.DownX, this.props.container.DownY)) + } + componentWillUnmount() { + if (this._reactionDisposer) { + this._reactionDisposer(); + } + this.cleanupInteractions(); + } + + + @action + cleanupInteractions = () => { + document.removeEventListener("keypress", this.onKeyPress, false); + } + + @action + onCursorPlaced = (visible: boolean, downX: number, downY: number): void => { + if (visible) { + document.addEventListener("keypress", this.onKeyPress, false); + this._lastX = downX; + this._lastY = downY; + } else + this.cleanupInteractions(); + } + + @action + onKeyPress = (e: KeyboardEvent) => { + // Mixing events between React and Native is finicky. In FormattedTextBox, we set the + // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore + // the keyPress here. + //if not these keys, make a textbox if preview cursor is active! + if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { + //make textbox and add it to this collection + let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY); + let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" }); + this.props.addLiveTextDocument(newBox); + e.stopPropagation(); + } + } + + render() { + //get local position and place cursor there! + let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY); + return ( + !this.props.container.PreviewCursorVisible ? (null) : +
I
) + + } +} \ No newline at end of file diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index f48be2047..b54744337 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -5,7 +5,7 @@ import { Key } from "../../../fields/Key"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; +import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; -- cgit v1.2.3-70-g09d2 From 1bd678851632bbbb302363574eb5e3e19dc343e9 Mon Sep 17 00:00:00 2001 From: bob Date: Fri, 29 Mar 2019 13:18:35 -0400 Subject: reorganized and mostly working northstar histograms. --- src/client/documents/Documents.ts | 13 +- src/client/northstar/core/filter/FilterModel.ts | 2 +- src/client/northstar/dash-fields/HistogramField.ts | 64 ++++ src/client/northstar/dash-nodes/HistogramBox.scss | 34 ++ src/client/northstar/dash-nodes/HistogramBox.tsx | 161 ++++++++++ .../dash-nodes/HistogramBoxPrimitives.scss | 26 ++ .../dash-nodes/HistogramBoxPrimitives.tsx | 341 +++++++++++++++++++++ .../dash-nodes/HistogramLabelPrimitives.scss | 13 + .../dash-nodes/HistogramLabelPrimitives.tsx | 78 +++++ src/client/northstar/model/ModelHelpers.ts | 18 +- .../model/binRanges/VisualBinRangeHelper.ts | 6 +- src/client/northstar/operations/BaseOperation.ts | 4 +- .../northstar/operations/HistogramOperation.ts | 13 +- src/client/northstar/utils/SizeConverter.ts | 6 +- src/client/views/Main.tsx | 39 +-- src/client/views/nodes/DocumentContentsView.tsx | 5 +- src/client/views/nodes/DocumentView.tsx | 3 - src/client/views/nodes/HistogramBox.scss | 22 -- src/client/views/nodes/HistogramBox.tsx | 105 ------- src/client/views/nodes/HistogramBoxPrimitives.scss | 25 -- src/client/views/nodes/HistogramBoxPrimitives.tsx | 336 -------------------- .../views/nodes/HistogramLabelPrimitives.scss | 12 - .../views/nodes/HistogramLabelPrimitives.tsx | 78 ----- src/fields/HistogramField.ts | 59 ---- src/fields/KeyStore.ts | 1 - src/server/ServerUtil.ts | 3 +- .../authentication/models/current_user_utils.ts | 27 +- 27 files changed, 789 insertions(+), 705 deletions(-) create mode 100644 src/client/northstar/dash-fields/HistogramField.ts create mode 100644 src/client/northstar/dash-nodes/HistogramBox.scss create mode 100644 src/client/northstar/dash-nodes/HistogramBox.tsx create mode 100644 src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss create mode 100644 src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx create mode 100644 src/client/northstar/dash-nodes/HistogramLabelPrimitives.scss create mode 100644 src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx delete mode 100644 src/client/views/nodes/HistogramBox.scss delete mode 100644 src/client/views/nodes/HistogramBox.tsx delete mode 100644 src/client/views/nodes/HistogramBoxPrimitives.scss delete mode 100644 src/client/views/nodes/HistogramBoxPrimitives.tsx delete mode 100644 src/client/views/nodes/HistogramLabelPrimitives.scss delete mode 100644 src/client/views/nodes/HistogramLabelPrimitives.tsx delete mode 100644 src/fields/HistogramField.ts (limited to 'src/client/views/nodes/DocumentContentsView.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 837dfe815..663ccae61 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,6 +1,6 @@ import { AudioField } from "../../fields/AudioField"; import { Document } from "../../fields/Document"; -import { Field, FieldWaiting } from "../../fields/Field"; +import { Field } from "../../fields/Field"; import { HtmlField } from "../../fields/HtmlField"; import { ImageField } from "../../fields/ImageField"; import { InkField, StrokeData } from "../../fields/InkField"; @@ -11,6 +11,9 @@ import { PDFField } from "../../fields/PDFField"; import { TextField } from "../../fields/TextField"; import { VideoField } from "../../fields/VideoField"; import { WebField } from "../../fields/WebField"; +import { HistogramField } from "../northstar/dash-fields/HistogramField"; +import { HistogramBox } from "../northstar/dash-nodes/HistogramBox"; +import { HistogramOperation } from "../northstar/operations/HistogramOperation"; import { Server } from "../Server"; import { CollectionPDFView } from "../views/collections/CollectionPDFView"; import { CollectionVideoView } from "../views/collections/CollectionVideoView"; @@ -22,10 +25,6 @@ import { KeyValueBox } from "../views/nodes/KeyValueBox"; import { PDFBox } from "../views/nodes/PDFBox"; import { VideoBox } from "../views/nodes/VideoBox"; import { WebBox } from "../views/nodes/WebBox"; -import { HistogramBox } from "../views/nodes/HistogramBox"; -import { FieldView } from "../views/nodes/FieldView"; -import { HistogramField } from "../../fields/HistogramField"; -import { HistogramOperation } from "../northstar/operations/HistogramOperation"; export interface DocumentOptions { x?: number; @@ -44,7 +43,6 @@ export interface DocumentOptions { layoutKeys?: Key[]; viewType?: number; backgroundColor?: string; - northstarSchema?: string; } export namespace Documents { @@ -88,7 +86,6 @@ export namespace Documents { if (options.ink !== undefined) { doc.Set(KeyStore.Ink, new InkField(options.ink)); } if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); } if (options.layoutKeys !== undefined) { doc.Set(KeyStore.LayoutKeys, new ListField(options.layoutKeys)); } - if (options.northstarSchema !== undefined) { doc.SetText(KeyStore.NorthstarSchema, options.northstarSchema); } return doc; } @@ -126,7 +123,7 @@ export namespace Documents { function GetHistogramPrototype(): Document { if (!histoProto) { histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("AnnotationsKey"), - { x: 0, y: 0, width: 300, height: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }); + { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }); histoProto.SetText(KeyStore.BackgroundLayout, HistogramBox.LayoutString()); } return histoProto; diff --git a/src/client/northstar/core/filter/FilterModel.ts b/src/client/northstar/core/filter/FilterModel.ts index bc7938947..aee99d2b6 100644 --- a/src/client/northstar/core/filter/FilterModel.ts +++ b/src/client/northstar/core/filter/FilterModel.ts @@ -2,10 +2,10 @@ import { ValueComparison } from "./ValueComparision"; import { Utils } from "../../utils/Utils"; import { IBaseFilterProvider } from "./IBaseFilterProvider"; import { FilterOperand } from "./FilterOperand"; -import { HistogramField } from "../../../../fields/HistogramField"; import { KeyStore } from "../../../../fields/KeyStore"; import { FieldWaiting } from "../../../../fields/Field"; import { Document } from "../../../../fields/Document"; +import { HistogramField } from "../../dash-fields/HistogramField"; export class FilterModel { public ValueComparisons: ValueComparison[]; diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts new file mode 100644 index 000000000..00912c595 --- /dev/null +++ b/src/client/northstar/dash-fields/HistogramField.ts @@ -0,0 +1,64 @@ +import { BasicField } from "../../../fields/BasicField"; +import { Field, FieldId } from "../../../fields/Field"; +import { Types } from "../../../server/Message"; +import { HistogramOperation } from "../../../client/northstar/operations/HistogramOperation"; +import { action } from "mobx"; +import { AttributeTransformationModel } from "../../../client/northstar/core/attribute/AttributeTransformationModel"; +import { ColumnAttributeModel } from "../../../client/northstar/core/attribute/AttributeModel"; +import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; +import { ArrayUtil } from "../utils/ArrayUtil"; +import { Schema } from "../model/idea/idea"; + + +export class HistogramField extends BasicField { + constructor(data?: HistogramOperation, id?: FieldId, save: boolean = true) { + super(data ? data : HistogramOperation.Empty, save, id); + } + + toString(): string { + return JSON.stringify(this.Data); + } + + Copy(): Field { + return new HistogramField(this.Data); + } + + ToScriptString(): string { + return `new HistogramField("${this.Data}")`; + } + + ToJson(): { type: Types, data: string, _id: string } { + return { + type: Types.HistogramOp, + data: JSON.stringify(this.Data), + _id: this.Id + } + } + + @action + static FromJson(id: string, data: any): HistogramField { + let jp = JSON.parse(data); + let X: AttributeTransformationModel | undefined; + let Y: AttributeTransformationModel | undefined; + let V: AttributeTransformationModel | undefined; + + let schema = CurrentUserUtils.GetNorthstarSchema(jp.SchemaName); + if (schema) { + CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { + if (attr.displayName == jp.X.AttributeModel.Attribute.DisplayName) { + X = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.X.AggregateFunction); + } + if (attr.displayName == jp.Y.AttributeModel.Attribute.DisplayName) { + Y = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.Y.AggregateFunction); + } + if (attr.displayName == jp.V.AttributeModel.Attribute.DisplayName) { + V = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.V.AggregateFunction); + } + }); + if (X && Y && V) { + return new HistogramField(new HistogramOperation(jp.SchemaName, X, Y, V, jp.Normalization), id, false); + } + } + return new HistogramField(HistogramOperation.Empty, id, false); + } +} \ No newline at end of file diff --git a/src/client/northstar/dash-nodes/HistogramBox.scss b/src/client/northstar/dash-nodes/HistogramBox.scss new file mode 100644 index 000000000..b11840a65 --- /dev/null +++ b/src/client/northstar/dash-nodes/HistogramBox.scss @@ -0,0 +1,34 @@ +.histogrambox-container { + padding: 0vw; + position: absolute; + text-align: center; + width: 100%; + height: 100%; + background: black; + } + .histogrambox-xaxislabel { + position:absolute; + width:100%; + text-align: center; + bottom:0; + background: lightgray; + font-size: 14; + font-weight: bold; + } + .histogrambox-yaxislabel { + position:absolute; + height:100%; + width: 25px; + bottom:0; + background: lightgray; + } + .histogrambox-yaxislabel-text { + position:absolute; + transform-origin: left; + transform: rotate(-90deg); + text-align: center; + font-size: 14; + font-weight: bold; + bottom: calc(50% - 25px); + } + \ No newline at end of file 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 { + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(HistogramBox, fieldStr) } + private _dropXRef = React.createRef(); + private _dropYRef = React.createRef(); + 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) => 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 ( + runInAction(() => { this.PanelWidth = r.entry.width; this.PanelHeight = r.entry.height })}> + {({ measureRef }) => +
+
+ + {labelY} + +
+
+ + +
+
{labelX}
+
+ } +
+ ) + } +} + diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss new file mode 100644 index 000000000..9d42219cc --- /dev/null +++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss @@ -0,0 +1,26 @@ +.histogramboxprimitives-border { + border: 3px; + border-style: solid; + border-color: white; + pointer-events: none; + position: absolute; +} +.histogramboxprimitives-bar { + position: absolute; + border: 1px; + border-style: solid; + border-color: #282828; + pointer-events: all; +} + +.histogramboxprimitives-placer { + position: absolute; + pointer-events: none; + width: 100%; + height: 100%; +} +.histogramboxprimitives-line { + position: absolute; + background: darkGray; + opacity: 0.4; +} \ No newline at end of file diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx new file mode 100644 index 000000000..d2f1be4fd --- /dev/null +++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx @@ -0,0 +1,341 @@ +import React = require("react") +import { computed, observable, runInAction } 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 "./HistogramBoxPrimitives.scss"; + + +@observer +export class HistogramBoxPrimitives extends React.Component { + private get histoOp() { return this.props.HistoBox.HistoOp; } + private get renderDimension() { return this.props.HistoBox.SizeConverter.RenderDimension; } + @observable _selectedPrims: HistogramBinPrimitive[] = []; + @computed get xaxislines() { return this.renderGridLinesAndLabels(0); } + @computed get yaxislines() { return this.renderGridLinesAndLabels(1); } + @computed get selectedPrimitives() { return this._selectedPrims.map(bp => this.drawRect(bp.Rect, bp.BarAxis, undefined, "border")); } + @computed get binPrimitives() { + let histoResult = this.props.HistoBox.HistogramResult; + if (!histoResult || !histoResult.bins || !this.props.HistoBox.VisualBinRanges.length) + return (null); + 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 => + prims.push(...[{ r: bp.Rect, c: bp.Color }, { r: bp.MarginRect, c: StyleConstants.MARGIN_BARS_COLOR }].map(pair => this.drawRect(pair.r, bp.BarAxis, pair.c, "bar", toggle)))); + return prims; + }, [] as JSX.Element[]); + } + + private getSelectionToggle(binPrimitives: HistogramBinPrimitive[], allBrushIndex: number, filterModel: FilterModel) { + let allBrushPrim = ArrayUtil.FirstOrDefault(binPrimitives, bp => bp.BrushIndex == allBrushIndex); + return !allBrushPrim ? () => { } : () => runInAction(() => { + if (ArrayUtil.Contains(this.histoOp.FilterModels, filterModel)) { + this._selectedPrims.splice(this._selectedPrims.indexOf(allBrushPrim!), 1); + this.histoOp.RemoveFilterModels([filterModel]); + } + else { + this._selectedPrims.push(allBrushPrim!); + this.histoOp.AddFilterModels([filterModel]); + } + }) + } + + private renderGridLinesAndLabels(axis: number) { + if (!this.props.HistoBox.SizeConverter.Initialized) + return (null); + let labels = this.props.HistoBox.VisualBinRanges[axis].GetLabels(); + return labels.reduce((prims, binLabel, i) => { + let r = this.props.HistoBox.SizeConverter.DataToScreenRange(binLabel.minValue!, binLabel.maxValue!, axis); + prims.push(this.drawLine(r.xFrom, r.yFrom, axis == 0 ? 0 : r.xTo - r.xFrom, axis == 0 ? r.yTo - r.yFrom : 0)); + if (i == labels.length - 1) + prims.push(this.drawLine(axis == 0 ? r.xTo : r.xFrom, axis == 0 ? r.yFrom : r.yTo, axis == 0 ? 0 : r.xTo - r.xFrom, axis == 0 ? r.yTo - r.yFrom : 0)); + return prims; + }, [] as JSX.Element[]); + } + + drawEntity(xFrom: number, yFrom: number, entity: JSX.Element) { + let transXpercent = xFrom / this.renderDimension * 100; + let transYpercent = yFrom / this.renderDimension * 100; + return (
+ {entity} +
); + } + drawLine(xFrom: number, yFrom: number, width: number, height: number) { + if (height < 0) { + yFrom += height; + height = -height; + } + if (width < 0) { + xFrom += width; + width = -width; + } + let trans2Xpercent = width == 0 ? `1px` : `${(xFrom + width) / this.renderDimension * 100}%`; + let trans2Ypercent = height == 0 ? `1px` : `${(yFrom + height) / this.renderDimension * 100}%`; + let line = (
); + 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; + r.height = -r.height; + } + if (r.width < 0) { + r.x += r.width; + r.width = -r.width; + } + let widthPercent = r.width / this.renderDimension * 100; + let heightPercent = r.height / this.renderDimension * 100; + let rect = (
{ if (e.button == 0) tapHandler() }} + style={{ + borderBottomStyle: barAxis == 1 ? "none" : "solid", + borderLeftStyle: barAxis == 0 ? "none" : "solid", + width: `${widthPercent}%`, + height: `${heightPercent}%`, + background: color ? `${LABColor.RGBtoHexString(color)}` : "" + }} + />); + return this.drawEntity(r.x, r.y, rect); + } + render() { + return
+ {this.xaxislines} + {this.yaxislines} + {this.binPrimitives} + {this.selectedPrimitives} +
+ } +} + +class HistogramBinPrimitive { + constructor(init?: Partial) { + 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 = new Array(); + 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.X.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 { + 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 (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.scss b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.scss new file mode 100644 index 000000000..304d33771 --- /dev/null +++ b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.scss @@ -0,0 +1,13 @@ + + .histogramLabelPrimitives-gridlabel { + position:absolute; + transform-origin: left top; + font-size: 11; + color:white; + } + .histogramLabelPrimitives-placer { + position:absolute; + width:100%; + height:100%; + pointer-events: none; + } \ No newline at end of file diff --git a/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx new file mode 100644 index 000000000..45b23874d --- /dev/null +++ b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx @@ -0,0 +1,78 @@ +import React = require("react") +import { action, computed, reaction } from "mobx"; +import { observer } from "mobx-react"; +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 "./HistogramLabelPrimitives.scss"; + +@observer +export class HistogramLabelPrimitives extends React.Component { + componentDidMount() { + reaction(() => [this.props.HistoBox.PanelWidth, this.props.HistoBox.SizeConverter.LeftOffset, this.props.HistoBox.VisualBinRanges.length], + (fields) => HistogramLabelPrimitives.computeLabelAngle(fields[0] as number, fields[1] as number, this.props.HistoBox), { fireImmediately: true }); + } + + @action + static computeLabelAngle(panelWidth: number, leftOffset: number, histoBox: HistogramBox) { + const textWidth = 30; + if (panelWidth > 0 && histoBox.VisualBinRanges.length && histoBox.VisualBinRanges[0] instanceof NominalVisualBinRange) { + let space = (panelWidth - leftOffset * 2) / histoBox.VisualBinRanges[0].GetBins().length; + histoBox.SizeConverter.SetLabelAngle(Math.min(Math.PI / 2, Math.max(Math.PI / 6, textWidth / space * Math.PI / 2))); + } else if (histoBox.SizeConverter.LabelAngle) { + histoBox.SizeConverter.SetLabelAngle(0); + } + } + @computed get xaxislines() { return this.renderGridLinesAndLabels(0); } + @computed get yaxislines() { return this.renderGridLinesAndLabels(1); } + + private renderGridLinesAndLabels(axis: number) { + let sc = this.props.HistoBox.SizeConverter; + let vb = this.props.HistoBox.VisualBinRanges; + if (!vb.length || !sc.Initialized) + return (null); + let dim = (axis == 0 ? this.props.HistoBox.PanelWidth : this.props.HistoBox.PanelHeight) / ((axis == 0 && vb[axis] instanceof NominalVisualBinRange) ? + (12 + 5) : // (FontStyles.AxisLabel.fontSize + 5))); + sc.MaxLabelSizes[axis].coords[axis] + 5); + + let labels = vb[axis].GetLabels(); + return labels.reduce((prims, binLabel, i) => { + let r = sc.DataToScreenRange(binLabel.minValue!, binLabel.maxValue!, axis); + if (i % Math.ceil(labels.length / dim) === 0 && binLabel.label) { + const label = binLabel.label.Truncate(StyleConstants.MAX_CHAR_FOR_HISTOGRAM_LABELS, "..."); + const textHeight = 14; const textWidth = 30; + let xStart = (axis === 0 ? r.xFrom + (r.xTo - r.xFrom) / 2.0 : r.xFrom - 10 - textWidth); + let yStart = (axis === 1 ? r.yFrom - textHeight / 2 : r.yFrom); + + if (axis == 0 && vb[axis] instanceof NominalVisualBinRange) { + let space = (r.xTo - r.xFrom) / sc.RenderDimension * this.props.HistoBox.PanelWidth; + xStart += Math.max(textWidth / 2, (1 - textWidth / space) * textWidth / 2) - textHeight / 2; + } + + let xPercent = axis == 1 ? `${xStart}px` : `${xStart / sc.RenderDimension * 100}%` + let yPercent = axis == 0 ? `${this.props.HistoBox.PanelHeight - sc.BottomOffset - textHeight}px` : `${yStart / sc.RenderDimension * 100}%` + + prims.push( +
+
+ {label} +
+
+ ) + } + return prims; + }, [] as JSX.Element[]); + } + + render() { + let xaxislines = this.xaxislines; + let yaxislines = this.yaxislines; + return
+ {xaxislines} + {yaxislines} +
+ } + +} \ No newline at end of file diff --git a/src/client/northstar/model/ModelHelpers.ts b/src/client/northstar/model/ModelHelpers.ts index 8de0bd260..d0711fb69 100644 --- a/src/client/northstar/model/ModelHelpers.ts +++ b/src/client/northstar/model/ModelHelpers.ts @@ -13,11 +13,11 @@ import { CurrentUserUtils } from "../../../server/authentication/models/current_ export class ModelHelpers { - public static CreateAggregateKey(atm: AttributeTransformationModel, histogramResult: HistogramResult, + public static CreateAggregateKey(distinctAttributeParameters: AttributeParameters | undefined, atm: AttributeTransformationModel, histogramResult: HistogramResult, brushIndex: number, aggParameters?: SingleDimensionAggregateParameters): AggregateKey { { if (aggParameters == undefined) { - aggParameters = ModelHelpers.GetAggregateParameter(atm); + aggParameters = ModelHelpers.GetAggregateParameter(distinctAttributeParameters, atm); } else { aggParameters.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel); @@ -34,40 +34,40 @@ export class ModelHelpers { return ArrayUtil.IndexOfWithEqual(histogramResult.aggregateParameters!, aggParameters); } - public static GetAggregateParameter(atm: AttributeTransformationModel): AggregateParameters | undefined { + public static GetAggregateParameter(distinctAttributeParameters: AttributeParameters | undefined, atm: AttributeTransformationModel): AggregateParameters | undefined { var aggParam: AggregateParameters | undefined; if (atm.AggregateFunction === AggregateFunction.Avg) { var avg = new AverageAggregateParameters(); avg.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel); - avg.distinctAttributeParameters = CurrentUserUtils.ActiveSchema!.distinctAttributeParameters; + avg.distinctAttributeParameters = distinctAttributeParameters; aggParam = avg; } else if (atm.AggregateFunction === AggregateFunction.Count) { var cnt = new CountAggregateParameters(); cnt.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel); - cnt.distinctAttributeParameters = CurrentUserUtils.ActiveSchema!.distinctAttributeParameters; + cnt.distinctAttributeParameters = distinctAttributeParameters; aggParam = cnt; } else if (atm.AggregateFunction === AggregateFunction.Sum) { var sum = new SumAggregateParameters(); sum.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel); - sum.distinctAttributeParameters = CurrentUserUtils.ActiveSchema!.distinctAttributeParameters; + sum.distinctAttributeParameters = distinctAttributeParameters; aggParam = sum; } return aggParam; } - public static GetAggregateParametersWithMargins(atms: Array): Array { + public static GetAggregateParametersWithMargins(distinctAttributeParameters: AttributeParameters | undefined, atms: Array): Array { var aggregateParameters = new Array(); atms.forEach(agg => { - var aggParams = ModelHelpers.GetAggregateParameter(agg); + var aggParams = ModelHelpers.GetAggregateParameter(distinctAttributeParameters, agg); if (aggParams) { aggregateParameters.push(aggParams); var margin = new MarginAggregateParameters() margin.aggregateFunction = agg.AggregateFunction; margin.attributeParameters = ModelHelpers.GetAttributeParameters(agg.AttributeModel); - margin.distinctAttributeParameters = CurrentUserUtils.ActiveSchema!.distinctAttributeParameters; + margin.distinctAttributeParameters = distinctAttributeParameters; aggregateParameters.push(margin); } }); diff --git a/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts index 9eae39800..53d585bb4 100644 --- a/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts +++ b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts @@ -1,4 +1,4 @@ -import { BinRange, NominalBinRange, QuantitativeBinRange, Exception, AlphabeticBinRange, DateTimeBinRange, AggregateBinRange, DoubleValueAggregateResult, HistogramResult } from "../idea/idea"; +import { BinRange, NominalBinRange, QuantitativeBinRange, Exception, AlphabeticBinRange, DateTimeBinRange, AggregateBinRange, DoubleValueAggregateResult, HistogramResult, AttributeParameters } from "../idea/idea"; import { VisualBinRange, ChartType } from "./VisualBinRange"; import { NominalVisualBinRange } from "./NominalVisualBinRange"; import { QuantitativeVisualBinRange } from "./QuantitativeVisualBinRange"; @@ -30,13 +30,13 @@ export class VisualBinRangeHelper { throw new Exception() } - public static GetVisualBinRange(dataBinRange: BinRange, histoResult: HistogramResult, attr: AttributeTransformationModel, chartType: ChartType): VisualBinRange { + public static GetVisualBinRange(distinctAttributeParameters: AttributeParameters | undefined, dataBinRange: BinRange, histoResult: HistogramResult, attr: AttributeTransformationModel, chartType: ChartType): VisualBinRange { if (!(dataBinRange instanceof AggregateBinRange)) { return VisualBinRangeHelper.GetNonAggregateVisualBinRange(dataBinRange); } else { - var aggregateKey = ModelHelpers.CreateAggregateKey(attr, histoResult, ModelHelpers.AllBrushIndex(histoResult)); + var aggregateKey = ModelHelpers.CreateAggregateKey(distinctAttributeParameters, attr, histoResult, ModelHelpers.AllBrushIndex(histoResult)); var minValue = Number.MAX_VALUE; var maxValue = Number.MIN_VALUE; for (var b = 0; b < histoResult.brushes!.length; b++) { diff --git a/src/client/northstar/operations/BaseOperation.ts b/src/client/northstar/operations/BaseOperation.ts index 7db6fcb91..94e0849af 100644 --- a/src/client/northstar/operations/BaseOperation.ts +++ b/src/client/northstar/operations/BaseOperation.ts @@ -10,9 +10,9 @@ 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 = undefined; + public OperationReference?: OperationReference = undefined; private static _nextId = 0; public RequestSalt: string = ""; diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts index 6c7288d42..8a0f648f6 100644 --- a/src/client/northstar/operations/HistogramOperation.ts +++ b/src/client/northstar/operations/HistogramOperation.ts @@ -27,6 +27,8 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons @observable public V: AttributeTransformationModel; @observable public BrusherModels: BrushLinkModel[] = []; @observable public BrushableModels: BrushLinkModel[] = []; + @observable public SchemaName: string; + @computed public get Schema() { return CurrentUserUtils.GetNorthstarSchema(this.SchemaName); } @action public AddFilterModels(filterModels: FilterModel[]): void { @@ -38,23 +40,24 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons } public getValue(axis: number, bin: Bin, result: HistogramResult, brushIndex: number) { - var aggregateKey = ModelHelpers.CreateAggregateKey(axis == 0 ? this.X : axis == 1 ? this.Y : this.V, result, brushIndex); + 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(new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute()))); + 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(x: AttributeTransformationModel, y: AttributeTransformationModel, v: AttributeTransformationModel, normalized?: number) { + constructor(schemaName: string, x: AttributeTransformationModel, y: AttributeTransformationModel, v: AttributeTransformationModel, normalized?: number) { super(); this.X = x; this.Y = y; this.V = v; this.Normalization = normalized ? normalized : -1; + this.SchemaName = schemaName; } @computed @@ -93,7 +96,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons allAttributes = ArrayUtil.Distinct(allAttributes.filter(a => a.AggregateFunction !== AggregateFunction.None)); let numericDataTypes = [DataType.Int, DataType.Double, DataType.Float]; - let perBinAggregateParameters: AggregateParameters[] = ModelHelpers.GetAggregateParametersWithMargins(allAttributes); + let perBinAggregateParameters: AggregateParameters[] = ModelHelpers.GetAggregateParametersWithMargins(this.Schema!.distinctAttributeParameters, allAttributes); let globalAggregateParameters: AggregateParameters[] = []; [histoX, histoY] .filter(a => a.AggregateFunction === AggregateFunction.None && ArrayUtil.Contains(numericDataTypes, a.AttributeModel.DataType)) @@ -112,7 +115,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons let [perBinAggregateParameters, globalAggregateParameters] = this.GetAggregateParameters(this.X, this.Y, this.V); return new HistogramOperationParameters({ enableBrushComputation: true, - adapterName: CurrentUserUtils.ActiveSchema!.displayName, + adapterName: this.SchemaName, filter: this.FilterString, brushes: this.BrushString, binningParameters: [ModelHelpers.GetBinningParameters(this.X, SETTINGS_X_BINS, this.QRange ? this.QRange.minValue : undefined, this.QRange ? this.QRange.maxValue : undefined), diff --git a/src/client/northstar/utils/SizeConverter.ts b/src/client/northstar/utils/SizeConverter.ts index ffd162a83..30627dfd5 100644 --- a/src/client/northstar/utils/SizeConverter.ts +++ b/src/client/northstar/utils/SizeConverter.ts @@ -68,10 +68,14 @@ export class SizeConverter { return [undefined, undefined]; } - public DataToScreenAxisRange(visualBinRanges: VisualBinRange[], index: number, bin: Bin) { + public DataToScreenXAxisRange(visualBinRanges: VisualBinRange[], index: number, bin: Bin) { var value = visualBinRanges[0].GetValueFromIndex(bin.binIndex!.indices![index]); return [this.DataToScreenX(value), this.DataToScreenX(visualBinRanges[index].AddStep(value))] } + public DataToScreenYAxisRange(visualBinRanges: VisualBinRange[], index: number, bin: Bin) { + var value = visualBinRanges[1].GetValueFromIndex(bin.binIndex!.indices![index]); + return [this.DataToScreenY(value), this.DataToScreenY(visualBinRanges[index].AddStep(value))] + } public DataToScreenX(x: number): number { return ((x - this.DataMins[0]) / this.DataRanges[0]) * this.RenderDimension; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 09778ac77..2f20b102c 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -58,7 +58,7 @@ export class Main extends React.Component { @observable private mainfreeform?: Document; @observable public pwidth: number = 0; @observable public pheight: number = 0; - private _northstarColumns: Document[] = []; + private _northstarSchemas: Document[] = []; @computed private get mainContainer(): Document | undefined { let doc = this.userDocument.GetT(KeyStore.ActiveWorkspace, Document); @@ -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._northstarColumns, { width: 200, height: 200, title: "a tree collection" })); + let addTreeNode = action(() => Documents.TreeDocument(this._northstarSchemas, { width: 100, 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" })); @@ -334,24 +334,27 @@ export class Main extends React.Component { // --------------- Northstar hooks ------------- / @action SetNorthstarCatalog(ctlog: Catalog) { + CurrentUserUtils.NorthstarDBCatalog = ctlog; if (ctlog && ctlog.schemas) { - CurrentUserUtils.ActiveSchema = ArrayUtil.FirstOrDefault(ctlog.schemas!, (s: Schema) => s.displayName === "mimic"); - CurrentUserUtils.GetAllNorthstarColumnAttributes().map(attr => { - Server.GetField(attr.displayName!, action((field: Opt) => { - if (field instanceof Document) { - this._northstarColumns.push(field); - } else { - var atmod = new ColumnAttributeModel(attr); - let histoOp = new HistogramOperation( - new AttributeTransformationModel(atmod, AggregateFunction.None), - new AttributeTransformationModel(atmod, AggregateFunction.Count), - new AttributeTransformationModel(atmod, AggregateFunction.Count)); - this._northstarColumns.push(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName!, northstarSchema: CurrentUserUtils.ActiveSchema!.displayName! }, attr.displayName!)); - } - })); + this._northstarSchemas = ctlog.schemas.map(schema => { + 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) => { + if (field instanceof Document) { + schemaDocuments.push(field); + } else { + var atmod = new ColumnAttributeModel(attr); + let histoOp = new HistogramOperation(schema!.displayName!, + 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!)); + } + })); + }); + return schemaDoc; }) - console.log("Activating schema " + CurrentUserUtils.ActiveSchema!.displayName!) - CurrentUserUtils.ActiveSchemaName = CurrentUserUtils.ActiveSchema!.displayName!; } } async initializeNorthstar(): Promise { diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index b54744337..77551649c 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -19,8 +19,7 @@ import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; -import { HistogramBox } from "./HistogramBox"; -import { HistogramBoxPrimitives } from "./HistogramBoxPrimitives"; +import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import React = require("react"); const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -54,7 +53,7 @@ export class DocumentContentsView extends React.ComponentError loading layout keys

; } return { } private dropDisposer?: DragManager.DragDropDisposer; - protected createDropTarget = (ele: HTMLDivElement) => { - - } componentDidMount() { if (this._mainCont.current) { diff --git a/src/client/views/nodes/HistogramBox.scss b/src/client/views/nodes/HistogramBox.scss deleted file mode 100644 index 2660b1b75..000000000 --- a/src/client/views/nodes/HistogramBox.scss +++ /dev/null @@ -1,22 +0,0 @@ -.histogrambox-container { - padding: 0vw; - position: absolute; - text-align: center; - width: 100%; - height: 100%; - } - .histogrambox-xaxislabel { - position:absolute; - width:100%; - text-align: center; - bottom:0; - font-size: 14; - font-weight: bold; - } - - .histogrambox-container { - position:absolute; - width:100%; - height: 100%; - } - \ No newline at end of file diff --git a/src/client/views/nodes/HistogramBox.tsx b/src/client/views/nodes/HistogramBox.tsx deleted file mode 100644 index 3307925a2..000000000 --- a/src/client/views/nodes/HistogramBox.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React = require("react") -import { computed, observable, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import Measure from "react-measure"; -import { Dictionary } from "typescript-collections"; -import { Opt } from "../../../fields/Field"; -import { HistogramField } from "../../../fields/HistogramField"; -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 } 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 "./../../northstar/utils/Extensions"; -import { FieldView, FieldViewProps } from './FieldView'; -import "./HistogramBox.scss"; -import { HistogramBoxPrimitives } from './HistogramBoxPrimitives'; -import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives"; - -export interface HistogramPrimitivesProps { - HistoBox: HistogramBox; -} - -@observer -export class HistogramBox extends React.Component { - public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(HistogramBox, fieldStr) } - public HitTargets: Dictionary = new Dictionary(); - - @observable public PanelWidth: number = 100; - @observable public PanelHeight: number = 100; - @observable public HistoOp?: HistogramOperation; - @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; - } - - componentDidMount() { - reaction(() => [CurrentUserUtils.ActiveSchemaName, this.props.doc.GetText(KeyStore.NorthstarSchema, "?")], - (params: string[]) => params[0] === params[1] && this.activateHistogramOperation(), { 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(br, this.HistogramResult!, ind ? this.HistoOp!.Y : this.HistoOp!.X, this.ChartType))); - - let valueAggregateKey = ModelHelpers.CreateAggregateKey(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.MIN_VALUE, Number.MAX_VALUE]); - } - }); - } - - activateHistogramOperation() { - this.props.doc.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt) => { - if (histoOp) { - runInAction(() => this.HistoOp = histoOp.Data); - 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 label = this.HistoOp && this.HistoOp.X ? this.HistoOp.X.AttributeModel.DisplayName : "<...>"; - 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 ( - runInAction(() => { this.PanelWidth = r.entry.width; this.PanelHeight = r.entry.height })}> - {({ measureRef }) => -
-
- - -
-
{label}
-
- } -
- ) - } -} - diff --git a/src/client/views/nodes/HistogramBoxPrimitives.scss b/src/client/views/nodes/HistogramBoxPrimitives.scss deleted file mode 100644 index 85f2c092d..000000000 --- a/src/client/views/nodes/HistogramBoxPrimitives.scss +++ /dev/null @@ -1,25 +0,0 @@ -.histogramboxprimitives-border { - border: 3px; - border-style: solid; - border-color: #282828; - pointer-events: none; - position: absolute; -} -.histogramboxprimitives-bar { - position: absolute; - border: 1px; - border-style: solid; - border-color: #282828; - pointer-events: all; -} - -.histogramboxprimitives-placer { - position: absolute; - pointer-events: none; - width: 100%; - height: 100%; -} -.histogramboxprimitives-line { - position: absolute; - background: lightgray; -} \ No newline at end of file diff --git a/src/client/views/nodes/HistogramBoxPrimitives.tsx b/src/client/views/nodes/HistogramBoxPrimitives.tsx deleted file mode 100644 index f15cb5689..000000000 --- a/src/client/views/nodes/HistogramBoxPrimitives.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import React = require("react") -import { computed, observable, runInAction, trace } from "mobx"; -import { observer } from "mobx-react"; -import { Utils as DashUtils } from '../../../Utils'; -import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel"; -import { ChartType } from '../../northstar/model/binRanges/VisualBinRange'; -import { AggregateFunction, Bin, Brush, HistogramResult, MarginAggregateParameters, MarginAggregateResult, BinLabel } 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 "./HistogramBoxPrimitives.scss"; -import { HistogramOperation } from "../../northstar/operations/HistogramOperation"; -import { FilterModel } from "../../northstar/core/filter/FilterModel"; - - -@observer -export class HistogramBoxPrimitives extends React.Component { - private get histoOp() { return this.props.HistoBox.HistoOp; } - private get renderDimension() { return this.props.HistoBox.SizeConverter.RenderDimension; } - @observable _selectedPrims: HistogramBinPrimitive[] = []; - @computed get xaxislines() { return this.renderGridLinesAndLabels(0); } - @computed get yaxislines() { return this.renderGridLinesAndLabels(1); } - @computed get selectedPrimitives() { return this._selectedPrims.map(bp => this.drawRect(bp.Rect, bp.BarAxis, undefined, "border")); } - @computed get binPrimitives() { - let histoResult = this.props.HistoBox.HistogramResult; - if (!histoResult || !histoResult.bins || !this.props.HistoBox.VisualBinRanges.length) - return (null); - let allBrushIndex = ModelHelpers.AllBrushIndex(histoResult); - return Object.keys(histoResult.bins).reduce((prims, key) => { - let drawPrims = new HistogramBinPrimitiveCollection(histoResult!.bins![key], this.props.HistoBox); - let filterModel = ModelHelpers.GetBinFilterModel(histoResult!.bins![key], allBrushIndex, histoResult!, this.histoOp!.X, this.histoOp!.Y); - - this.props.HistoBox.HitTargets.setValue(drawPrims.HitGeom, filterModel); - - let toggle = this.getSelectionToggle(drawPrims.BinPrimitives, allBrushIndex, filterModel); - drawPrims.BinPrimitives.filter(bp => bp.DataValue && bp.BrushIndex !== allBrushIndex).map(bp => - prims.push(...[{ r: bp.Rect, c: bp.Color }, { r: bp.MarginRect, c: StyleConstants.MARGIN_BARS_COLOR }].map(pair => this.drawRect(pair.r, bp.BarAxis, pair.c, "bar", toggle)))); - return prims; - }, [] as JSX.Element[]); - } - - private getSelectionToggle(binPrimitives: HistogramBinPrimitive[], allBrushIndex: number, filterModel: FilterModel) { - let allBrushPrim = ArrayUtil.FirstOrDefault(binPrimitives, bp => bp.BrushIndex == allBrushIndex); - return !allBrushPrim ? () => { } : () => runInAction(() => { - if (ArrayUtil.Contains(this.histoOp!.FilterModels, filterModel)) { - this._selectedPrims.splice(this._selectedPrims.indexOf(allBrushPrim!), 1); - this.histoOp!.RemoveFilterModels([filterModel]); - } - else { - this._selectedPrims.push(allBrushPrim!); - this.histoOp!.AddFilterModels([filterModel]); - } - }) - } - - private renderGridLinesAndLabels(axis: number) { - if (!this.props.HistoBox.SizeConverter.Initialized) - return (null); - let labels = this.props.HistoBox.VisualBinRanges[axis].GetLabels(); - return labels.reduce((prims, binLabel, i) => { - let r = this.props.HistoBox.SizeConverter.DataToScreenRange(binLabel.minValue!, binLabel.maxValue!, axis); - prims.push(this.drawLine(r.xFrom, r.yFrom, axis == 0 ? 0 : r.xTo - r.xFrom, axis == 0 ? r.yTo - r.yFrom : 0)); - if (i == labels.length - 1) - prims.push(this.drawLine(axis == 0 ? r.xTo : r.xFrom, axis == 0 ? r.yFrom : r.yTo, axis == 0 ? 0 : r.xTo - r.xFrom, axis == 0 ? r.yTo - r.yFrom : 0)); - return prims; - }, [] as JSX.Element[]); - } - - drawEntity(xFrom: number, yFrom: number, entity: JSX.Element) { - let transXpercent = xFrom / this.renderDimension * 100; - let transYpercent = yFrom / this.renderDimension * 100; - return (
- {entity} -
); - } - drawLine(xFrom: number, yFrom: number, width: number, height: number) { - if (height < 0) { - yFrom += height; - height = -height; - } - if (width < 0) { - xFrom += width; - width = -width; - } - let trans2Xpercent = width == 0 ? `1px` : `${(xFrom + width) / this.renderDimension * 100}%`; - let trans2Ypercent = height == 0 ? `1px` : `${(yFrom + height) / this.renderDimension * 100}%`; - let line = (
); - return this.drawEntity(xFrom, yFrom, line); - } - - drawRect(r: PIXIRectangle, barAxis: number, color: number | undefined, classExt: string, tapHandler: () => void = () => { }) { - let widthPercent = r.width / this.renderDimension * 100; - let heightPercent = r.height / this.renderDimension * 100; - let rect = (
{ if (e.button == 0) tapHandler() }} - style={{ - borderBottomStyle: barAxis == 1 ? "none" : "solid", - borderLeftStyle: barAxis == 0 ? "none" : "solid", - width: `${widthPercent}%`, - height: `${heightPercent}%`, - background: color ? `${LABColor.RGBtoHexString(color)}` : "" - }} - />); - return this.drawEntity(r.x, r.y, rect); - } - render() { - return
- {this.xaxislines} - {this.yaxislines} - {this.binPrimitives} - {this.selectedPrimitives} -
- } -} - -class HistogramBinPrimitive { - constructor(init?: Partial) { - 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 = new Array(); - 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.X.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.DataToScreenAxisRange(this._histoBox.VisualBinRanges, 0, bin); - let [yFrom, yTo] = this.sizeConverter.DataToScreenAxisRange(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.X, this.histoResult, brush.brushIndex!)); - let [yFrom, yTo] = this.sizeConverter.DataToScreenPointRange(1, bin, ModelHelpers.CreateAggregateKey(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.DataToScreenAxisRange(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.DataToScreenAxisRange(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(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 { - 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 (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/views/nodes/HistogramLabelPrimitives.scss b/src/client/views/nodes/HistogramLabelPrimitives.scss deleted file mode 100644 index d8ee88d72..000000000 --- a/src/client/views/nodes/HistogramLabelPrimitives.scss +++ /dev/null @@ -1,12 +0,0 @@ - - .histogramLabelPrimitives-gridlabel { - position:absolute; - transform-origin: left top; - font-size: 11; - } - .histogramLabelPrimitives-placer { - position:absolute; - width:100%; - height:100%; - pointer-events: none; - } \ No newline at end of file diff --git a/src/client/views/nodes/HistogramLabelPrimitives.tsx b/src/client/views/nodes/HistogramLabelPrimitives.tsx deleted file mode 100644 index 7f365e45b..000000000 --- a/src/client/views/nodes/HistogramLabelPrimitives.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React = require("react") -import { action, computed, reaction } from "mobx"; -import { observer } from "mobx-react"; -import { Utils as DashUtils } from '../../../Utils'; -import { NominalVisualBinRange } from "../../northstar/model/binRanges/NominalVisualBinRange"; -import { StyleConstants } from "../../northstar/utils/StyleContants"; -import "./../../northstar/utils/Extensions"; -import "./HistogramLabelPrimitives.scss"; -import { HistogramBox, HistogramPrimitivesProps } from "./HistogramBox"; - -@observer -export class HistogramLabelPrimitives extends React.Component { - componentDidMount() { - reaction(() => [this.props.HistoBox.PanelWidth, this.props.HistoBox.SizeConverter.LeftOffset, this.props.HistoBox.VisualBinRanges.length], - (fields) => HistogramLabelPrimitives.computeLabelAngle(fields[0] as number, fields[1] as number, this.props.HistoBox), { fireImmediately: true }); - } - - @action - static computeLabelAngle(panelWidth: number, leftOffset: number, histoBox: HistogramBox) { - const textWidth = 30; - if (panelWidth > 0 && histoBox.VisualBinRanges.length && histoBox.VisualBinRanges[0] instanceof NominalVisualBinRange) { - let space = (panelWidth - leftOffset * 2) / histoBox.VisualBinRanges[0].GetBins().length; - histoBox.SizeConverter.SetLabelAngle(Math.min(Math.PI / 2, Math.max(Math.PI / 6, textWidth / space * Math.PI / 2))); - } else if (histoBox.SizeConverter.LabelAngle) { - histoBox.SizeConverter.SetLabelAngle(0); - } - } - @computed get xaxislines() { return this.renderGridLinesAndLabels(0); } - @computed get yaxislines() { return this.renderGridLinesAndLabels(1); } - - private renderGridLinesAndLabels(axis: number) { - let sc = this.props.HistoBox.SizeConverter; - let vb = this.props.HistoBox.VisualBinRanges; - if (!vb.length || !sc.Initialized) - return (null); - let dim = (axis == 0 ? this.props.HistoBox.PanelWidth : this.props.HistoBox.PanelHeight) / ((axis == 0 && vb[axis] instanceof NominalVisualBinRange) ? - (12 + 5) : // (FontStyles.AxisLabel.fontSize + 5))); - sc.MaxLabelSizes[axis].coords[axis] + 5); - - let labels = vb[axis].GetLabels(); - return labels.reduce((prims, binLabel, i) => { - let r = sc.DataToScreenRange(binLabel.minValue!, binLabel.maxValue!, axis); - if (i % Math.ceil(labels.length / dim) === 0 && binLabel.label) { - const label = binLabel.label.Truncate(StyleConstants.MAX_CHAR_FOR_HISTOGRAM_LABELS, "..."); - const textHeight = 14; const textWidth = 30; - let xStart = (axis === 0 ? r.xFrom + (r.xTo - r.xFrom) / 2.0 : r.xFrom - 10 - textWidth); - let yStart = (axis === 1 ? r.yFrom - textHeight / 2 : r.yFrom); - - if (axis == 0 && vb[axis] instanceof NominalVisualBinRange) { - let space = (r.xTo - r.xFrom) / sc.RenderDimension * this.props.HistoBox.PanelWidth; - xStart += Math.max(textWidth / 2, (1 - textWidth / space) * textWidth / 2) - textHeight / 2; - } - - let xPercent = axis == 1 ? `${xStart}px` : `${xStart / sc.RenderDimension * 100}%` - let yPercent = axis == 0 ? `${this.props.HistoBox.PanelHeight - sc.BottomOffset - textHeight}px` : `${yStart / sc.RenderDimension * 100}%` - - prims.push( -
-
- {label} -
-
- ) - } - return prims; - }, [] as JSX.Element[]); - } - - render() { - let xaxislines = this.xaxislines; - let yaxislines = this.yaxislines; - return
- {xaxislines} - {yaxislines} -
- } - -} \ No newline at end of file diff --git a/src/fields/HistogramField.ts b/src/fields/HistogramField.ts deleted file mode 100644 index bb0014ab3..000000000 --- a/src/fields/HistogramField.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { BasicField } from "./BasicField"; -import { Field, FieldId } from "./Field"; -import { Types } from "../server/Message"; -import { HistogramOperation } from "../client/northstar/operations/HistogramOperation"; -import { action } from "mobx"; -import { AttributeTransformationModel } from "../client/northstar/core/attribute/AttributeTransformationModel"; -import { ColumnAttributeModel } from "../client/northstar/core/attribute/AttributeModel"; -import { CurrentUserUtils } from "../server/authentication/models/current_user_utils"; - - -export class HistogramField extends BasicField { - constructor(data?: HistogramOperation, id?: FieldId, save: boolean = true) { - super(data ? data : HistogramOperation.Empty, save, id); - } - - toString(): string { - return JSON.stringify(this.Data); - } - - Copy(): Field { - return new HistogramField(this.Data); - } - - ToScriptString(): string { - return `new HistogramField("${this.Data}")`; - } - - ToJson(): { type: Types, data: string, _id: string } { - return { - type: Types.HistogramOp, - data: JSON.stringify(this.Data), - _id: this.Id - } - } - - @action - static FromJson(id: string, data: any): HistogramField { - let jp = JSON.parse(data); - let X: AttributeTransformationModel | undefined; - let Y: AttributeTransformationModel | undefined; - let V: AttributeTransformationModel | undefined; - - CurrentUserUtils.GetAllNorthstarColumnAttributes().map(attr => { - if (attr.displayName == jp.X.AttributeModel.Attribute.DisplayName) { - X = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.X.AggregateFunction); - } - if (attr.displayName == jp.Y.AttributeModel.Attribute.DisplayName) { - Y = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.Y.AggregateFunction); - } - if (attr.displayName == jp.V.AttributeModel.Attribute.DisplayName) { - V = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.V.AggregateFunction); - } - }); - if (X && Y && V) { - return new HistogramField(new HistogramOperation(X, Y, V, jp.Normalization), id, false); - } - return new HistogramField(HistogramOperation.Empty, id, false); - } -} \ No newline at end of file diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 09dddf962..aa0b9ce92 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -44,5 +44,4 @@ export namespace KeyStore { export const Archives = new Key("Archives"); export const Updated = new Key("Updated"); export const Workspaces = new Key("Workspaces"); - export const NorthstarSchema = new Key("NorthstarSchema"); } diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts index f958df04b..98a7a1451 100644 --- a/src/server/ServerUtil.ts +++ b/src/server/ServerUtil.ts @@ -17,8 +17,7 @@ import { VideoField } from '../fields/VideoField'; import { InkField } from '../fields/InkField'; import { PDFField } from '../fields/PDFField'; import { TupleField } from '../fields/TupleField'; -import { HistogramField } from '../fields/HistogramField'; - +import { HistogramField } from '../client/northstar/dash-fields/HistogramField'; diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 4b42e40b6..0ac85b446 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -7,8 +7,9 @@ import { Document } from "../../../fields/Document"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; import { Documents } from "../../../client/documents/Documents"; -import { Schema, Attribute, AttributeGroup } from "../../../client/northstar/model/idea/idea"; +import { Schema, Attribute, AttributeGroup, Catalog } from "../../../client/northstar/model/idea/idea"; import { observable, computed, action } from "mobx"; +import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil"; export class CurrentUserUtils { private static curr_email: string; @@ -16,8 +17,7 @@ export class CurrentUserUtils { private static user_document: Document; //TODO tfs: these should be temporary... private static mainDocId: string | undefined; - private static activeSchema: Schema | undefined; - @observable public static ActiveSchemaName: string = ""; + @observable private static catalog?: Catalog; public static get email(): string { return this.curr_email; @@ -39,12 +39,18 @@ export class CurrentUserUtils { this.mainDocId = id; } - - public static get ActiveSchema(): Schema | undefined { - return this.activeSchema; + @computed public static get NorthstarDBCatalog(): Catalog | undefined { + return this.catalog; + } + public static set NorthstarDBCatalog(ctlog: Catalog | undefined) { + this.catalog = ctlog; } - public static GetAllNorthstarColumnAttributes() { - if (!this.ActiveSchema || !this.ActiveSchema.rootAttributeGroup) { + public static GetNorthstarSchema(name: string): Schema | undefined { + return !this.catalog || !this.catalog.schemas ? undefined : + ArrayUtil.FirstOrDefault(this.catalog.schemas, (s: Schema) => s.displayName === name); + } + public static GetAllNorthstarColumnAttributes(schema: Schema) { + if (!schema || !schema.rootAttributeGroup) { return []; } const recurs = (attrs: Attribute[], g: AttributeGroup) => { @@ -56,13 +62,10 @@ export class CurrentUserUtils { } }; const allAttributes: Attribute[] = new Array(); - recurs(allAttributes, this.ActiveSchema.rootAttributeGroup); + recurs(allAttributes, schema.rootAttributeGroup); return allAttributes; } - public static set ActiveSchema(id: Schema | undefined) { - this.activeSchema = id; - } private static createUserDocument(id: string): Document { let doc = new Document(id); -- cgit v1.2.3-70-g09d2