diff options
author | bob <bcz@cs.brown.edu> | 2020-02-04 15:56:38 -0500 |
---|---|---|
committer | bob <bcz@cs.brown.edu> | 2020-02-04 15:56:38 -0500 |
commit | 7eb6f1bcc35d852308e2c2ea466d9727d3ba10ef (patch) | |
tree | a5d54bcc5fd9d41c25dfd95413460bf23d36a93e /src | |
parent | e6c1c5f08bf706023335936bebf463184650a6c2 (diff) |
pivot view/time view working
Diffstat (limited to 'src')
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) })); |