aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbob <bcz@cs.brown.edu>2020-02-04 15:56:38 -0500
committerbob <bcz@cs.brown.edu>2020-02-04 15:56:38 -0500
commit7eb6f1bcc35d852308e2c2ea466d9727d3ba10ef (patch)
treea5d54bcc5fd9d41c25dfd95413460bf23d36a93e /src
parente6c1c5f08bf706023335936bebf463184650a6c2 (diff)
pivot view/time view working
Diffstat (limited to 'src')
-rw-r--r--src/client/views/collections/CollectionSubView.tsx1
-rw-r--r--src/client/views/collections/CollectionTimeView.scss3
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx61
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx65
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx4
5 files changed, 85 insertions, 49 deletions
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 9cdd48089..a2700e75a 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -43,6 +43,7 @@ export interface SubCollectionViewProps extends CollectionViewProps {
children?: never | (() => JSX.Element[]) | React.ReactNode;
isAnnotationOverlay?: boolean;
annotationsKey: string;
+ layoutEngine?: () => string;
}
export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss
index 3aac2bc75..df5057b5b 100644
--- a/src/client/views/collections/CollectionTimeView.scss
+++ b/src/client/views/collections/CollectionTimeView.scss
@@ -8,6 +8,9 @@
.collectionFreeform-customText {
text-align: left;
}
+ .collectionFreeform-customDiv {
+ position: absolute;
+ }
.collectionTimeView-thumb {
position: absolute;
width: 30px;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index c92ffdeea..f999067d3 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,6 +1,6 @@
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable } from "mobx";
+import { action, computed, IReactionDisposer, observable, trace } from "mobx";
import { observer } from "mobx-react";
import { Set } from "typescript-collections";
import { Doc, DocListCast } from "../../../new_fields/Doc";
@@ -22,8 +22,10 @@ import { RichTextField } from "../../../new_fields/RichTextField";
@observer
export class CollectionTimeView extends CollectionSubView(doc => doc) {
+ _changing = false;
+ @observable _layoutEngine = "pivot";
+
componentDidMount() {
- this.props.Document._freeformLayoutEngine = "timeline";
const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked;
if (!this.props.Document._facetCollection) {
const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true });
@@ -190,14 +192,14 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
document.removeEventListener("pointermove", this.onMidUp);
}
+ layoutEngine = () => this._layoutEngine;
@computed get contents() {
return <div className="collectionTimeView-innards" key="timeline" style={{ width: this.bodyPanelWidth() }}>
- <CollectionFreeFormView {...this.props} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} />
+ <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} />
</div>;
}
-
- _changing = false;
- render() {
+ @computed get filterView() {
+ trace();
const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
const flyout = (
<div className="collectionTimeView-flyout" style={{ width: `${this._facetWidth}` }}>
@@ -208,6 +210,22 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
</label>)}
</div>
);
+ return <div className="collectionTimeView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}>
+ <div className="collectionTimeView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
+ <div className="collectionTimeView-button">
+ <span className="collectionTimeView-span">Facet Filters</span>
+ <FontAwesomeIcon icon={faEdit} size={"lg"} />
+ </div>
+ </Flyout>
+ </div>
+ <div className="collectionTimeView-tree" key="tree">
+ <CollectionTreeView {...this.props} Document={facetCollection} />
+ </div>
+ </div>;
+ }
+
+ render() {
const newEditableViewProps = {
GetValue: () => "",
SetValue: (value: any) => {
@@ -230,23 +248,18 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
nonNumbers++;
}
});
- const doTimeline = nonNumbers / this.childDocs.length < 0.1;
- if (doTimeline !== (this.props.Document._freeformLayoutEngine === "timeline")) {
+ const doTimeline = nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6;
+ if (doTimeline !== (this._layoutEngine === "timeline")) {
if (!this._changing) {
this._changing = true;
- setTimeout(() => {
- if (nonNumbers / this.childDocs.length > 0.1) {
- this.childDocs.map(child => child.isMinimized = false);
- this.props.Document._freeformLayoutEngine = "pivot";
- } else {
- this.props.Document._freeformLayoutEngine = "timeline";
- }
+ setTimeout(action(() => {
+ this._layoutEngine = doTimeline ? "timeline" : "pivot";
this._changing = false;
- }, 0);
+ }), 0);
}
- return (null);
}
+ const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
return !facetCollection ? (null) :
<div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
<div className={"pivotKeyEntry"}>
@@ -257,19 +270,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
<span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} />
</div>
}
- <div className="collectionTimeView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}>
- <div className="collectionTimeView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}>
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
- <div className="collectionTimeView-button">
- <span className="collectionTimeView-span">Facet Filters</span>
- <FontAwesomeIcon icon={faEdit} size={"lg"} />
- </div>
- </Flyout>
- </div>
- <div className="collectionTimeView-tree" key="tree">
- <CollectionTreeView {...this.props} Document={facetCollection} />
- </div>
- </div>
+ {this.filterView}
{this.contents}
{!this.props.isSelected() || !doTimeline ? (null) : <>
<div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 63bcc68e5..f35662dab 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -67,35 +67,59 @@ export function computePivotLayout(
panelDim: number[],
viewDefsToJSX: (views: any) => ViewDefResult[]
) {
- console.log("PIVOT " + pivotDoc[HeightSym]());
const fieldKey = "data";
const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3));
- let maxInColumn = 1;
const pivotFieldKey = toLabel(pivotDoc.pivotField);
for (const doc of childDocs) {
const val = Field.toString(doc[pivotFieldKey] as Field);
if (val) {
!pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []);
pivotColumnGroups.get(val)!.push(doc);
- maxInColumn = Math.max(maxInColumn, pivotColumnGroups.get(val)?.length || 0);
}
}
+ if (pivotColumnGroups.size > 10) {
+ const arrayofKeys = Array.from(pivotColumnGroups.keys());
+ const sortedKeys = arrayofKeys.sort();
+ const clusterSize = Math.ceil(pivotColumnGroups.size / 10);
+ const numClusters = Math.ceil(sortedKeys.length / clusterSize);
+ for (let i = 0; i < numClusters; i++) {
+ for (let j = i * clusterSize + 1; j < Math.min(sortedKeys.length, (i + 1) * clusterSize); j++) {
+ pivotColumnGroups.get(sortedKeys[i * clusterSize])!.push(...pivotColumnGroups.get(sortedKeys[j])!);
+ pivotColumnGroups.delete(sortedKeys[j]);
+ }
+ }
+ }
+ let maxInColumn = Array.from(pivotColumnGroups.values()).reduce((p, s) => Math.max(p, s.length), 1);
const colWidth = panelDim[0] / pivotColumnGroups.size;
const colHeight = panelDim[1];
- const pivotAxisWidth = Math.sqrt(colWidth * colHeight / maxInColumn);
- const numCols = Math.max(Math.round(colWidth / pivotAxisWidth), 1);
+ let numCols = 0;
+ let bestArea = 0;
+ let pivotAxisWidth = 0;
+ for (let i = 1; i < 10; i++) {
+ const numInCol = Math.ceil(maxInColumn / i);
+ const hd = colHeight / numInCol;
+ const wd = colWidth / i;
+ const dim = Math.min(hd, wd);
+ const area = dim * dim * i * numInCol;
+ if (area > bestArea) {
+ bestArea = area;
+ numCols = i;
+ pivotAxisWidth = dim;
+ }
+ }
const docMap = new Map<Doc, ViewDefBounds>();
- const groupNames: PivotData[] = [];;
+ const groupNames: PivotData[] = [];
const expander = 1.05;
const gap = .15;
let x = 0;
let max_text = 60;
- pivotColumnGroups.forEach((val, key) => {
+ Array.from(pivotColumnGroups.keys()).sort().forEach(key => {
+ const val = pivotColumnGroups.get(key)!;
let y = 0;
let xCount = 0;
const text = toLabel(key);
@@ -132,6 +156,10 @@ export function computePivotLayout(
x += pivotAxisWidth * (numCols * expander + gap);
});
+ const maxColHeight = pivotAxisWidth * Math.ceil(maxInColumn / numCols) + pivotAxisWidth;
+ const dividers = Array.from(pivotColumnGroups.values()).map((pg, i) =>
+ ({ type: "div", color: "lightGray", x: i * pivotAxisWidth * (numCols * expander + gap), y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, height: maxColHeight } as any));
+ groupNames.push(...dividers);
return normalizeResults(panelDim, max_text, childPairs, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
}
@@ -161,12 +189,10 @@ export function computeTimelineLayout(
}
let minTime = Number.MAX_VALUE;
- let maxTime = Number.MIN_VALUE;
+ let maxTime = -Number.MAX_VALUE;
childDocs.map(doc => {
const num = NumCast(doc[timelineFieldKey], Number(StrCast(doc[timelineFieldKey])));
- if (Number.isNaN(num) || (minTimeReq && num < minTimeReq) || (maxTimeReq && num > maxTimeReq)) {
- doc.isMinimized = true;
- } else {
+ if (!(Number.isNaN(num) || (minTimeReq && num < minTimeReq) || (maxTimeReq && num > maxTimeReq))) {
!pivotDateGroups.get(num) && pivotDateGroups.set(num, []);
pivotDateGroups.get(num)!.push(doc);
minTime = Math.min(num, minTime);
@@ -180,8 +206,14 @@ export function computeTimelineLayout(
minTime = curTime - (maxTime - curTime);
}
}
- pivotDoc[fieldKey + "-timelineMin"] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime;
- pivotDoc[fieldKey + "-timelineMax"] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime;
+ setTimeout(() => {
+ pivotDoc[fieldKey + "-timelineMin"] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime;
+ pivotDoc[fieldKey + "-timelineMax"] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime;
+ }, 0);
+
+ if (maxTime === minTime) {
+ maxTime = minTime + 1;
+ }
const arrayofKeys = Array.from(pivotDateGroups.keys());
const sortedKeys = arrayofKeys.sort((n1, n2) => n1 - n2);
@@ -204,7 +236,6 @@ export function computeTimelineLayout(
groupNames.push({ type: "text", text: curTime.toString(), x: (curTime - minTime) * scaling, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize });
}
const keyDocs = pivotDateGroups.get(key)!;
- keyDocs.forEach(d => d.isMinimized = false);
x += scaling * (key - prevKey);
const stack = findStack(x, stacking);
prevKey = key;
@@ -244,15 +275,15 @@ export function computeTimelineLayout(
function normalizeResults(panelDim: number[], fontHeight: number, childPairs: { data?: Doc, layout: Doc }[], docMap: Map<Doc, ViewDefBounds>,
poolData: Map<string, PoolData>, viewDefsToJSX: (views: any) => ViewDefResult[], groupNames: PivotData[], minWidth: number, extras: PivotData[]) {
- const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, height: gn.height }) as PivotData);
- const docEles = childPairs.filter(d => !d.layout.isMinimized).map(pair => docMap.get(pair.layout) as PivotData);
+ const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as PivotData);
+ const docEles = childPairs.filter(d => docMap.get(d.layout)).map(pair => docMap.get(pair.layout) as PivotData);
const aggBounds = aggregateBounds(docEles.concat(grpEles), 0, 0);
aggBounds.r = Math.max(minWidth, aggBounds.r - aggBounds.x);
const wscale = panelDim[0] / (aggBounds.r - aggBounds.x);
let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? (panelDim[1]) / (aggBounds.b - aggBounds.y) : wscale;
if (Number.isNaN(scale)) scale = 1;
- childPairs.filter(d => !d.layout.isMinimized).map(pair => {
+ childPairs.filter(d => docMap.get(d.layout)).map(pair => {
const newPosRaw = docMap.get(pair.layout);
if (newPosRaw) {
const newPos = {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 63544a637..6b81eba7d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -816,7 +816,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed get doInternalLayoutComputation() {
const newPool = new Map<string, any>();
- switch (this.Document._freeformLayoutEngine) {
+ switch (this.props.layoutEngine?.()) {
case "timeline": return { newPool, computedElementData: this.doTimelineLayout(newPool) };
case "pivot": return { newPool, computedElementData: this.doPivotLayout(newPool) };
}
@@ -839,7 +839,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)}
dataProvider={this.childDataProvider}
jitterRotation={NumCast(this.props.Document.jitterRotation)}
- fitToBox={this.props.fitToBox || this.Document._freeformLayoutEngine !== undefined} />,
+ fitToBox={this.props.fitToBox || this.props.layoutEngine !== undefined} />,
bounds: this.childDataProvider(pair.layout)
}));