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 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (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) }} -- 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