diff options
Diffstat (limited to 'src')
8 files changed, 456 insertions, 510 deletions
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index a626772e4..e31c04c40 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -98,7 +98,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; componentDidMount() { - this.props.setContentView?.(this); + this._props.setContentView?.(this); if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData(); } @@ -110,19 +110,19 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // toggles for user to decide which chart type to view the data in @computed get renderVizView() { - const scale = this.props.NativeDimScaling?.() || 1; + const scale = this._props.NativeDimScaling?.() || 1; const sharedProps = { Document: this.Document, layoutDoc: this.layoutDoc, records: this.records, axes: this.axes, - height: (this.props.PanelHeight() / scale - 32) /* height of 'change view' button */ * 0.9, - width: (this.props.PanelWidth() / scale) * 0.9, + height: (this._props.PanelHeight() / scale - 32) /* height of 'change view' button */ * 0.9, + width: (this._props.PanelWidth() / scale) * 0.9, margin: { top: 10, right: 25, bottom: 75, left: 45 }, }; if (!this.records.length) return 'no data/visualization'; switch (this.dataVizView) { - case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.props.DocumentView} selectAxes={this.selectAxes} />; + case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this._props.DocumentView} selectAxes={this.selectAxes} />; case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} @@ -131,7 +131,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } render() { - const scale = this.props.NativeDimScaling?.() || 1; + const scale = this._props.NativeDimScaling?.() || 1; return !this.records.length ? ( // displays how to get data into the DataVizBox if its empty @@ -140,7 +140,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div className="dataViz" style={{ - pointerEvents: this.props.isContentActive() === true ? 'all' : 'none', + pointerEvents: this._props.isContentActive() === true ? 'all' : 'none', width: `${100 / scale}%`, height: `${100 / scale}%`, transform: `scale(${scale})`, diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index a1bd316f0..d786e84ad 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -1,7 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ColorPicker, EditableText, IconButton, Size, Type } from 'browndash-components'; import * as d3 from 'd3'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaFillDrip } from 'react-icons/fa'; @@ -14,6 +14,7 @@ import { undoable } from '../../../../util/UndoManager'; import { PinProps, PresBox } from '../../trails'; import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils'; import './Chart.scss'; +import { ObservableReactComponent } from '../../../ObservableReactComponent'; export interface HistogramProps { Document: Doc; @@ -33,7 +34,7 @@ export interface HistogramProps { } @observer -export class Histogram extends React.Component<HistogramProps> { +export class Histogram extends ObservableReactComponent<HistogramProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _histogramRef: React.RefObject<HTMLDivElement> = React.createRef(); private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; @@ -45,46 +46,51 @@ export class Histogram extends React.Component<HistogramProps> { private selectedData: any = undefined; // Selection of selected bar private hoverOverData: any = undefined; // Selection of bar being hovered over + constructor(props: any) { + super(props); + makeObservable(this); + } + @computed get _tableDataIds() { - return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) @computed get _tableData() { - return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } // filters all data to just display selected data if brushed (created from an incoming link) @computed get _histogramData() { - if (this.props.axes.length < 1) return []; - if (this.props.axes.length < 2) { - var ax0 = this.props.axes[0]; - if (/\d/.test(this.props.records[0][ax0])) { + if (this._props.axes.length < 1) return []; + if (this._props.axes.length < 2) { + var ax0 = this._props.axes[0]; + if (/\d/.test(this._props.records[0][ax0])) { this.numericalXData = true; } - return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] })); + return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] })); } - var ax0 = this.props.axes[0]; - var ax1 = this.props.axes[1]; - if (/\d/.test(this.props.records[0][ax0])) { + var ax0 = this._props.axes[0]; + var ax1 = this._props.axes[1]; + if (/\d/.test(this._props.records[0][ax0])) { this.numericalXData = true; } - if (/\d/.test(this.props.records[0][ax1])) { + if (/\d/.test(this._props.records[0][ax1])) { this.numericalYData = true; } - return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]], [ax1]: record[this.props.axes[1]] })); + return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]], [ax1]: record[this._props.axes[1]] })); } @computed get defaultGraphTitle() { - var ax0 = this.props.axes[0]; - var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined; - if (this.props.axes.length < 2 || !ax1 || !/\d/.test(this.props.records[0][ax1]) || !this.numericalYData) { + var ax0 = this._props.axes[0]; + var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; + if (this._props.axes.length < 2 || !ax1 || !/\d/.test(this._props.records[0][ax1]) || !this.numericalYData) { return ax0 + ' Histogram'; } else return ax0 + ' by ' + ax1 + ' Histogram'; } @computed get parentViz() { - return DocCast(this.props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this.props.Document) // out of all links - // .filter(link => link.link_anchor_1 == this.props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link + return DocCast(this._props.Document.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links + // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @@ -114,16 +120,16 @@ export class Histogram extends React.Component<HistogramProps> { const anchor = Docs.Create.ConfigDocument({ title: 'histogram doc selection' + this._currSelected, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.Document); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); return anchor; }; @computed get height() { - return this.props.height - this.props.margin.top - this.props.margin.bottom; + return this._props.height - this._props.margin.top - this._props.margin.bottom; } @computed get width() { - return this.props.width - this.props.margin.left - this.props.margin.right; + return this._props.width - this._props.margin.left - this._props.margin.right; } // cleans data by converting numerical data to numbers and taking out empty cells @@ -211,10 +217,10 @@ export class Histogram extends React.Component<HistogramProps> { .select(this._histogramRef.current) .append('svg') .attr('class', 'graph') - .attr('width', width + this.props.margin.right + this.props.margin.left) - .attr('height', height + this.props.margin.top + this.props.margin.bottom) + .attr('width', width + this._props.margin.right + this._props.margin.left) + .attr('height', height + this._props.margin.top + this._props.margin.bottom) .append('g') - .attr('transform', 'translate(' + this.props.margin.left + ',' + this.props.margin.top + ')')); + .attr('transform', 'translate(' + this._props.margin.left + ',' + this._props.margin.top + ')')); var x = d3 .scaleLinear() .domain(this.numericalXData ? [startingPoint!, endingPoint!] : [0, numBins]) @@ -382,7 +388,7 @@ export class Histogram extends React.Component<HistogramProps> { ) .attr('fill', d => { var barColor; - const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); + const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); barColors.forEach(each => { if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; else { @@ -390,24 +396,24 @@ export class Histogram extends React.Component<HistogramProps> { if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); - return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor); + return barColor ? StrCast(barColor) : StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor); }); }; @action changeSelectedColor = (color: string) => { this.curBarSelected.attr('fill', color); - const barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); - const barColors = Cast(this.props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); + const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); barColors.push(StrCast(barName + '::' + color)); }; @action eraseSelectedColor = () => { - this.curBarSelected.attr('fill', this.props.layoutDoc.dataViz_histogram_defaultColor); - const barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + this.curBarSelected.attr('fill', this._props.layoutDoc.dataViz_histogram_defaultColor); + const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); - const barColors = Cast(this.props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); + const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); }; @@ -416,7 +422,7 @@ export class Histogram extends React.Component<HistogramProps> { if (svg) svg.selectAll('rect').attr('fill', (d: any) => { var barColor; - const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); + const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); barColors.forEach(each => { if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; else { @@ -424,7 +430,7 @@ export class Histogram extends React.Component<HistogramProps> { if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); - return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor); + return barColor ? StrCast(barColor) : StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor); }); }; @@ -433,14 +439,14 @@ export class Histogram extends React.Component<HistogramProps> { this._histogramData; var curSelectedBarName = ''; var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0]; - if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; - if (!this.props.layoutDoc.dataViz_histogram_defaultColor) this.props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2'; - if (!this.props.layoutDoc.dataViz_histogram_barColors) this.props.layoutDoc.dataViz_histogram_barColors = new List<string>(); + if (this._props.axes.length == 2) titleAccessor = 'dataViz_histogram_title' + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor = 'dataViz_histogram_title' + this._props.axes[0]; + if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; + if (!this._props.layoutDoc.dataViz_histogram_defaultColor) this._props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2'; + if (!this._props.layoutDoc.dataViz_histogram_barColors) this._props.layoutDoc.dataViz_histogram_barColors = new List<string>(); var selected = 'none'; if (this._currSelected) { - curSelectedBarName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); selected = '{ '; Object.keys(this._currSelected).forEach(key => key // @@ -450,17 +456,17 @@ export class Histogram extends React.Component<HistogramProps> { selected = selected.substring(0, selected.length - 2) + ' }'; } var selectedBarColor; - var barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::')); + var barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::')); barColors.forEach(each => each[0] === curSelectedBarName && (selectedBarColor = each[1])); if (this._histogramData.length > 0 || !this.parentViz) { - return this.props.axes.length >= 1 ? ( + return this._props.axes.length >= 1 ? ( <div className="chart-container"> <div className="graph-title"> <EditableText - val={StrCast(this.props.layoutDoc[titleAccessor])} + val={StrCast(this._props.layoutDoc[titleAccessor])} setVal={undoable( - action(val => (this.props.layoutDoc[titleAccessor] = val as string)), + action(val => (this._props.layoutDoc[titleAccessor] = val as string)), 'Change Graph Title' )} color={'black'} @@ -472,9 +478,9 @@ export class Histogram extends React.Component<HistogramProps> { tooltip={'Change Default Bar Color'} type={Type.SEC} icon={<FaFillDrip />} - selectedColor={StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor)} - setFinalColor={undoable(color => (this.props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} - setSelectedColor={undoable(color => (this.props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} + selectedColor={StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor)} + setFinalColor={undoable(color => (this._props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} + setSelectedColor={undoable(color => (this._props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} size={Size.XSMALL} /> </div> diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 7e0391879..abb95aa4c 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -1,6 +1,6 @@ import { EditableText, Size } from 'browndash-components'; import * as d3 from 'd3'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, NumListCast, StrListCast } from '../../../../../fields/Doc'; @@ -14,6 +14,7 @@ import { PinProps, PresBox } from '../../trails'; import { DataVizBox } from '../DataVizBox'; import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; import './Chart.scss'; +import { ObservableReactComponent } from '../../../ObservableReactComponent'; export interface DataPoint { x: number; @@ -40,33 +41,38 @@ export interface LineChartProps { } @observer -export class LineChart extends React.Component<LineChartProps> { +export class LineChart extends ObservableReactComponent<LineChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; @observable _currSelected: SelectedDataPoint | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates + constructor(props:any) { + super(props); + makeObservable(this); + } + @computed get _tableDataIds() { - return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) @computed get _tableData() { - return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } @computed get _lineChartData() { - var guids = StrListCast(this.props.layoutDoc.dataViz_rowIds); - if (this.props.axes.length <= 1) return []; - return this._tableData.map(record => ({ x: Number(record[this.props.axes[0]]), y: Number(record[this.props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); + var guids = StrListCast(this._props.layoutDoc.dataViz_rowIds); + if (this._props.axes.length <= 1) return []; + return this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); } @computed get graphTitle() { - return this.props.axes[1] + ' vs. ' + this.props.axes[0] + ' Line Chart'; + return this._props.axes[1] + ' vs. ' + this._props.axes[0] + ' Line Chart'; } @computed get parentViz() { - return DocCast(this.props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this.props.Document) // out of all links + return DocCast(this._props.Document.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links // .filter(link => { - // return link.link_anchor_1 == this.props.Document.dataViz_parentViz; + // return link.link_anchor_1 == this._props.Document.dataViz_parentViz; // }) // get links where this chart doc is the target of the link // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @@ -94,7 +100,7 @@ export class LineChart extends React.Component<LineChartProps> { { fireImmediately: true } ); this._disposers.annos = reaction( - () => DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), + () => DocListCast(this._props.dataDoc[this._props.fieldKey + '_annotations']), annotations => { // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way // could be blue colored to make it look like anchor @@ -114,7 +120,7 @@ export class LineChart extends React.Component<LineChartProps> { // redraw annotations when the chart data has changed, or the local or inherited selection has changed this.clearAnnotations(); selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true); - incomingHighlited?.forEach((record: any) => this.drawAnnotations(Number(record[this.props.axes[0]]), Number(record[this.props.axes[1]]))); + incomingHighlited?.forEach((record: any) => this.drawAnnotations(Number(record[this._props.axes[0]]), Number(record[this._props.axes[1]]))); }, { fireImmediately: true } ); @@ -170,23 +176,23 @@ export class LineChart extends React.Component<LineChartProps> { // title: 'line doc selection' + this._currSelected?.x, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.Document); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); anchor.config_dataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined; return anchor; }; @computed get height() { - return this.props.height - this.props.margin.top - this.props.margin.bottom; + return this._props.height - this._props.margin.top - this._props.margin.bottom; } @computed get width() { - return this.props.width - this.props.margin.left - this.props.margin.right; + return this._props.width - this._props.margin.left - this._props.margin.right; } @computed get defaultGraphTitle() { - var ax0 = this.props.axes[0]; - var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined; - if (this.props.axes.length < 2 || !/\d/.test(this.props.records[0][ax0]) || !ax1) { + var ax0 = this._props.axes[0]; + var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; + if (this._props.axes.length < 2 || !/\d/.test(this._props.records[0][ax0]) || !ax1) { return ax0 + ' Line Chart'; } else return ax1 + ' by ' + ax0 + ' Line Chart'; } @@ -210,7 +216,7 @@ export class LineChart extends React.Component<LineChartProps> { // TODO: nda - get rid of svg element in the list? if (this._currSelected && this._currSelected.x == x && this._currSelected.y == y) this._currSelected = undefined; else this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; - this.props.records.forEach(record => record[this.props.axes[0]] === x && record[this.props.axes[1]] === y && (record.selected = true)); + this._props.records.forEach(record => record[this._props.axes[0]] === x && record[this._props.axes[1]] === y && (record.selected = true)); } drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { @@ -245,7 +251,7 @@ export class LineChart extends React.Component<LineChartProps> { const yScale = scaleCreatorNumerical(0, yMax, height, 0); // adding svg - const margin = this.props.margin; + const margin = this._props.margin; const svg = (this._lineChartSvg = d3 .select(this._lineChartRef.current) .append('svg') @@ -317,7 +323,7 @@ export class LineChart extends React.Component<LineChartProps> { svg.append('text') .attr('transform', 'translate(' + width / 2 + ' ,' + (height + 40) + ')') .style('text-anchor', 'middle') - .text(this.props.axes[0]); + .text(this._props.axes[0]); svg.append('text') .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') .attr('x', -(height / 2)) @@ -325,7 +331,7 @@ export class LineChart extends React.Component<LineChartProps> { .attr('height', 20) .attr('width', 20) .style('text-anchor', 'middle') - .text(this.props.axes[1]); + .text(this._props.axes[1]); }; private updateTooltip( @@ -346,18 +352,18 @@ export class LineChart extends React.Component<LineChartProps> { render() { var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0]; - if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; - const selectedPt = this._currSelected ? `{ ${this.props.axes[0]}: ${this._currSelected.x} ${this.props.axes[1]}: ${this._currSelected.y} }` : 'none'; + if (this._props.axes.length == 2) titleAccessor = 'dataViz_lineChart_title' + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + this._props.axes[0]; + if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; + const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none'; if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) { - return this.props.axes.length >= 2 && /\d/.test(this.props.records[0][this.props.axes[0]]) && /\d/.test(this.props.records[0][this.props.axes[1]]) ? ( + return this._props.axes.length >= 2 && /\d/.test(this._props.records[0][this._props.axes[0]]) && /\d/.test(this._props.records[0][this._props.axes[1]]) ? ( <div className="chart-container"> <div className="graph-title"> <EditableText - val={StrCast(this.props.layoutDoc[titleAccessor])} + val={StrCast(this._props.layoutDoc[titleAccessor])} setVal={undoable( - action(val => (this.props.layoutDoc[titleAccessor] = val as string)), + action(val => (this._props.layoutDoc[titleAccessor] = val as string)), 'Change Graph Title' )} color={'black'} diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index c2e03744e..9e7265b26 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -1,7 +1,7 @@ import { Checkbox } from '@mui/material'; import { ColorPicker, EditableText, Size, Type } from 'browndash-components'; import * as d3 from 'd3'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaFillDrip } from 'react-icons/fa'; @@ -13,6 +13,7 @@ import { Docs } from '../../../../documents/Documents'; import { undoable } from '../../../../util/UndoManager'; import { PinProps, PresBox } from '../../trails'; import './Chart.scss'; +import { ObservableReactComponent } from '../../../ObservableReactComponent'; export interface PieChartProps { Document: Doc; @@ -32,7 +33,7 @@ export interface PieChartProps { } @observer -export class PieChart extends React.Component<PieChartProps> { +export class PieChart extends ObservableReactComponent<PieChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; @@ -41,44 +42,49 @@ export class PieChart extends React.Component<PieChartProps> { private hoverOverData: any = undefined; // Selection of slice being hovered over @observable _currSelected: any | undefined = undefined; // Object of selected slice + constructor(props: any) { + super(props); + makeObservable(this); + } + @computed get _tableDataIds() { - return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) @computed get _tableData() { - return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } // organized by specified number percentages/ratios if one column is selected and it contains numbers // otherwise, assume data is organized by categories @computed get byCategory() { - return !/\d/.test(this.props.records[0][this.props.axes[0]]) || this.props.layoutDoc.dataViz_pie_asHistogram; + return !/\d/.test(this._props.records[0][this._props.axes[0]]) || this._props.layoutDoc.dataViz_pie_asHistogram; } // filters all data to just display selected data if brushed (created from an incoming link) @computed get _pieChartData() { - if (this.props.axes.length < 1) return []; + if (this._props.axes.length < 1) return []; - const ax0 = this.props.axes[0]; - if (this.props.axes.length < 2) { - return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] })); + const ax0 = this._props.axes[0]; + if (this._props.axes.length < 2) { + return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] })); } - const ax1 = this.props.axes[1]; - return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]], [ax1]: record[this.props.axes[1]] })); + const ax1 = this._props.axes[1]; + return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]], [ax1]: record[this._props.axes[1]] })); } @computed get defaultGraphTitle() { - var ax0 = this.props.axes[0]; - var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined; - if (this.props.axes.length < 2 || !/\d/.test(this.props.records[0][ax0]) || !ax1) { + var ax0 = this._props.axes[0]; + var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; + if (this._props.axes.length < 2 || !/\d/.test(this._props.records[0][ax0]) || !ax1) { return ax0 + ' Pie Chart'; } return ax1 + ' by ' + ax0 + ' Pie Chart'; } @computed get parentViz() { - return DocCast(this.props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this.props.Document) // out of all links - // .filter(link => link.link_anchor_1 == this.props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link + return DocCast(this._props.Document.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links + // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @@ -101,16 +107,16 @@ export class PieChart extends React.Component<PieChartProps> { // title: 'piechart doc selection' + this._currSelected, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.Document); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); return anchor; }; @computed get height() { - return this.props.height - this.props.margin.top - this.props.margin.bottom; + return this._props.height - this._props.margin.top - this._props.margin.bottom; } @computed get width() { - return this.props.width - this.props.margin.left - this.props.margin.right; + return this._props.width - this._props.margin.left - this._props.margin.right; } // cleans data by converting numerical data to numbers and taking out empty cells @@ -181,7 +187,7 @@ export class PieChart extends React.Component<PieChartProps> { var percentField = Object.keys(dataSet[0])[0]; var descriptionField = Object.keys(dataSet[0])[1]!; - var radius = Math.min(width, height - this.props.margin.top - this.props.margin.bottom) / 2; + var radius = Math.min(width, height - this._props.margin.top - this._props.margin.bottom) / 2; // converts data into Objects var data = this.data(dataSet); @@ -209,10 +215,10 @@ export class PieChart extends React.Component<PieChartProps> { .select(this._piechartRef.current) .append('svg') .attr('class', 'graph') - .attr('width', width + this.props.margin.right + this.props.margin.left) - .attr('height', height + this.props.margin.top + this.props.margin.bottom) + .attr('width', width + this._props.margin.right + this._props.margin.left) + .attr('height', height + this._props.margin.top + this._props.margin.bottom) .append('g')); - let g = svg.append('g').attr('transform', 'translate(' + (width / 2 + this.props.margin.left) + ',' + height / 2 + ')'); + let g = svg.append('g').attr('transform', 'translate(' + (width / 2 + this._props.margin.left) + ',' + height / 2 + ')'); var pie = d3.pie(); var arc = d3.arc().innerRadius(0).outerRadius(radius); @@ -249,7 +255,7 @@ export class PieChart extends React.Component<PieChartProps> { } catch (error) {} possibleDataPointVals.push(dataPointVal); }); - const sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); + const sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); arcs.append('path') .attr('fill', (d, i) => { var dataPoint; @@ -261,7 +267,7 @@ export class PieChart extends React.Component<PieChartProps> { } var sliceColor; if (dataPoint) { - const sliceTitle = dataPoint[this.props.axes[0]]; + const sliceTitle = dataPoint[this._props.axes[0]]; const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; sliceColors.forEach(each => each[0] == accessByName && (sliceColor = each[1])); } @@ -309,10 +315,10 @@ export class PieChart extends React.Component<PieChartProps> { @action changeSelectedColor = (color: string) => { this.curSliceSelected.attr('fill', color); - const sliceTitle = this._currSelected[this.props.axes[0]]; + const sliceTitle = this._currSelected[this._props.axes[0]]; const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; - const sliceColors = Cast(this.props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null); + const sliceColors = Cast(this._props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null); sliceColors.map(each => { if (each.split('::')[0] == sliceName) sliceColors.splice(sliceColors.indexOf(each), 1); }); @@ -320,20 +326,20 @@ export class PieChart extends React.Component<PieChartProps> { }; @action changeHistogramCheckBox = () => { - this.props.layoutDoc.dataViz_pie_asHistogram = !this.props.layoutDoc.dataViz_pie_asHistogram; + this._props.layoutDoc.dataViz_pie_asHistogram = !this._props.layoutDoc.dataViz_pie_asHistogram; this.drawChart(this._pieChartData, this.width, this.height); }; render() { var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + this.props.axes[0]; - if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; - if (!this.props.layoutDoc.dataViz_pie_sliceColors) this.props.layoutDoc.dataViz_pie_sliceColors = new List<string>(); + if (this._props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + this._props.axes[0]; + if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; + if (!this._props.layoutDoc.dataViz_pie_sliceColors) this._props.layoutDoc.dataViz_pie_sliceColors = new List<string>(); var selected: string; var curSelectedSliceName = ''; if (this._currSelected) { - const sliceTitle = this._currSelected[this.props.axes[0]]; + const sliceTitle = this._currSelected[this._props.axes[0]]; curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; selected = '{ '; Object.keys(this._currSelected).map(key => { @@ -343,19 +349,19 @@ export class PieChart extends React.Component<PieChartProps> { selected += ' }'; } else selected = 'none'; var selectedSliceColor; - var sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); + var sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); sliceColors.forEach(each => { if (each[0] == curSelectedSliceName!) selectedSliceColor = each[1]; }); if (this._pieChartData.length > 0 || !this.parentViz) { - return this.props.axes.length >= 1 ? ( + return this._props.axes.length >= 1 ? ( <div className="chart-container"> <div className="graph-title"> <EditableText - val={StrCast(this.props.layoutDoc[titleAccessor])} + val={StrCast(this._props.layoutDoc[titleAccessor])} setVal={undoable( - action(val => (this.props.layoutDoc[titleAccessor] = val as string)), + action(val => (this._props.layoutDoc[titleAccessor] = val as string)), 'Change Graph Title' )} color={'black'} @@ -363,9 +369,9 @@ export class PieChart extends React.Component<PieChartProps> { fillWidth /> </div> - {this.props.axes.length === 1 && /\d/.test(this.props.records[0][this.props.axes[0]]) ? ( - <div className={'asHistogram-checkBox'} style={{ width: this.props.width }}> - <Checkbox color="primary" onChange={this.changeHistogramCheckBox} checked={this.props.layoutDoc.dataViz_pie_asHistogram as boolean} /> + {this._props.axes.length === 1 && /\d/.test(this._props.records[0][this._props.axes[0]]) ? ( + <div className={'asHistogram-checkBox'} style={{ width: this._props.width }}> + <Checkbox color="primary" onChange={this.changeHistogramCheckBox} checked={this._props.layoutDoc.dataViz_pie_asHistogram as boolean} /> Organize data as histogram </div> ) : null} diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 3e7d3af8c..f77c138df 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,5 +1,5 @@ import { Button, Type } from 'browndash-components'; -import { action, computed, IReactionDisposer, observable, reaction, trace } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, Field, NumListCast } from '../../../../../fields/Doc'; @@ -11,6 +11,7 @@ import { DragManager } from '../../../../util/DragManager'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; import './Chart.scss'; +import { ObservableReactComponent } from '../../../ObservableReactComponent'; const { default: { DATA_VIZ_TABLE_ROW_HEIGHT } } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore interface TableBoxProps { Document: Doc; @@ -30,13 +31,17 @@ interface TableBoxProps { } @observer -export class TableBox extends React.Component<TableBoxProps> { +export class TableBox extends ObservableReactComponent<TableBoxProps> { _inputChangedDisposer?: IReactionDisposer; _containerRef: HTMLDivElement | null = null; @observable _scrollTop = -1; @observable _tableHeight = 0; @observable _tableContainerHeight = 0; + constructor(props: any) { + super(props); + makeObservable(this); + } componentDidMount() { // if the tableData changes (ie., when records are selected by the parent (input) visulization), @@ -48,17 +53,17 @@ export class TableBox extends React.Component<TableBoxProps> { this._inputChangedDisposer?.(); } @computed get _tableDataIds() { - return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) @computed get _tableData() { - return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } @computed get parentViz() { - return DocCast(this.props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this.props.Document) // out of all links - // .filter(link => link.link_anchor_1 == this.props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link + return DocCast(this._props.Document.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links + // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @@ -68,14 +73,14 @@ export class TableBox extends React.Component<TableBoxProps> { // updates the 'dataViz_selectedRows' and 'dataViz_highlightedRows' fields to no longer include rows that aren't in the table filterSelectedRowsDown = () => { - const selected = NumListCast(this.props.layoutDoc.dataViz_selectedRows); - this.props.layoutDoc.dataViz_selectedRows = new List<number>(selected.filter(rowId => this._tableDataIds.includes(rowId))); // filters through selected to remove guids that were removed in the incoming data - const highlighted = NumListCast(this.props.layoutDoc.dataViz_highlitedRows); - this.props.layoutDoc.dataViz_highlitedRows = new List<number>(highlighted.filter(rowId => this._tableDataIds.includes(rowId))); // filters through highlighted to remove guids that were removed in the incoming data + const selected = NumListCast(this._props.layoutDoc.dataViz_selectedRows); + this._props.layoutDoc.dataViz_selectedRows = new List<number>(selected.filter(rowId => this._tableDataIds.includes(rowId))); // filters through selected to remove guids that were removed in the incoming data + const highlighted = NumListCast(this._props.layoutDoc.dataViz_highlitedRows); + this._props.layoutDoc.dataViz_highlitedRows = new List<number>(highlighted.filter(rowId => this._tableDataIds.includes(rowId))); // filters through highlighted to remove guids that were removed in the incoming data }; @computed get viewScale() { - return this.props.docView?.()?._props.ScreenToLocalTransform().Scale || 1; + return this._props.docView?.()?._props.ScreenToLocalTransform().Scale || 1; } @computed get rowHeight() { console.log('scale = ' + this.viewScale + ' table = ' + this._tableHeight + ' ids = ' + this._tableDataIds.length); @@ -89,14 +94,14 @@ export class TableBox extends React.Component<TableBoxProps> { return Math.ceil(this.startID + (this._tableContainerHeight * this.viewScale) / (this.rowHeight || 1)); } @action handleScroll = () => { - if (!this.props.docView?.()?.ContentDiv?.hidden) { + if (!this._props.docView?.()?.ContentDiv?.hidden) { this._scrollTop = this._containerRef?.scrollTop ?? 0; } }; @action tableRowClick = (e: React.MouseEvent, rowId: number) => { - const highlited = Cast(this.props.layoutDoc.dataViz_highlitedRows, listSpec('number'), null); - const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); + const highlited = Cast(this._props.layoutDoc.dataViz_highlitedRows, listSpec('number'), null); + const selected = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); if (e.metaKey) { // highlighting a row if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); @@ -120,26 +125,26 @@ export class TableBox extends React.Component<TableBoxProps> { e, e => { // dragging off a column to create a brushed DataVizBox - const sourceAnchorCreator = () => this.props.docView?.()!.Document!; + const sourceAnchorCreator = () => this._props.docView?.()!.Document!; const targetCreator = (annotationOn: Doc | undefined) => { - const embedding = Doc.MakeEmbedding(this.props.docView?.()!.Document!); + const embedding = Doc.MakeEmbedding(this._props.docView?.()!.Document!); embedding._dataViz = DataVizView.TABLE; embedding._dataViz_axes = new List<string>([col, col]); - embedding._dataViz_parentViz = this.props.Document; + embedding._dataViz_parentViz = this._props.Document; embedding.annotationOn = annotationOn; - embedding.histogramBarColors = Field.Copy(this.props.layoutDoc.histogramBarColors); - embedding.defaultHistogramColor = this.props.layoutDoc.defaultHistogramColor; - embedding.pieSliceColors = Field.Copy(this.props.layoutDoc.pieSliceColors); + embedding.histogramBarColors = Field.Copy(this._props.layoutDoc.histogramBarColors); + embedding.defaultHistogramColor = this._props.layoutDoc.defaultHistogramColor; + embedding.pieSliceColors = Field.Copy(this._props.layoutDoc.pieSliceColors); return embedding; }; - if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { - DragManager.StartAnchorAnnoDrag(e.target instanceof HTMLElement ? [e.target] : [], new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { + if (this._props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { + DragManager.StartAnchorAnnoDrag(e.target instanceof HTMLElement ? [e.target] : [], new DragManager.AnchorAnnoDragData(this._props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { dragComplete: e => { if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { e.linkDocument.link_displayLine = true; e.linkDocument.link_matchEmbeddings = true; e.linkDocument.link_displayArrow = true; - // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.Document; + // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this._props.Document; // e.annoDragData.linkSourceDoc.followLinkZoom = false; } }, @@ -150,11 +155,11 @@ export class TableBox extends React.Component<TableBoxProps> { }, emptyFunction, action(e => { - const newAxes = this.props.axes; + const newAxes = this._props.axes; if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); else if (newAxes.length > 1) newAxes[1] = col; else newAxes.push(col); - this.props.selectAxes(newAxes); + this._props.selectAxes(newAxes); }) ); }; @@ -166,14 +171,14 @@ export class TableBox extends React.Component<TableBoxProps> { className="tableBox" tabIndex={0} onKeyDown={e => { - if (this.props.layoutDoc && e.key === 'a' && (e.ctrlKey || e.metaKey)) { + if (this._props.layoutDoc && e.key === 'a' && (e.ctrlKey || e.metaKey)) { e.stopPropagation(); - this.props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); + this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); } }}> <div className="selectAll-buttons"> - <Button onClick={action(() => (this.props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} /> - <Button onClick={action(() => (this.props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} /> + <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} /> + <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} /> </div> <div className={`tableBox-container ${this.columns[0]}`} @@ -181,7 +186,7 @@ export class TableBox extends React.Component<TableBoxProps> { onScroll={this.handleScroll} ref={action((r: HTMLDivElement | null) => { this._containerRef = r; - if (!this.props.docView?.()?.ContentDiv?.hidden && r) { + if (!this._props.docView?.()?.ContentDiv?.hidden && r) { this._tableContainerHeight = r.getBoundingClientRect().height ?? 0; r.addEventListener( 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) @@ -196,7 +201,7 @@ export class TableBox extends React.Component<TableBoxProps> { <table className="tableBox-table" ref={action((r: HTMLTableElement | null) => { - if (!this.props.docView?.()?.ContentDiv?.hidden && r) { + if (!this._props.docView?.()?.ContentDiv?.hidden && r) { this._tableHeight = r?.getBoundingClientRect().height ?? 0; } })}> @@ -207,8 +212,8 @@ export class TableBox extends React.Component<TableBoxProps> { <th key={this.columns.indexOf(col)} style={{ - color: this.props.axes.slice().reverse().lastElement() === col ? 'darkgreen' : this.props.axes.lastElement() === col ? 'darkred' : undefined, - background: this.props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this.props.axes.lastElement() === col ? '#Fbdbdb' : undefined, + color: this._props.axes.slice().reverse().lastElement() === col ? 'darkgreen' : this._props.axes.lastElement() === col ? 'darkred' : undefined, + background: this._props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this._props.axes.lastElement() === col ? '#Fbdbdb' : undefined, fontWeight: 'bolder', border: '3px solid black', }} @@ -227,13 +232,13 @@ export class TableBox extends React.Component<TableBoxProps> { className={`tableBox-row ${this.columns[0]}`} onClick={e => this.tableRowClick(e, rowId)} style={{ - background: NumListCast(this.props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this.props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', + background: NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', }}> {this.columns.map(col => { - const colSelected = this.props.axes.length > 1 ? this.props.axes[0] == col || this.props.axes[1] == col : this.props.axes.length > 0 ? this.props.axes[0] == col : false; + const colSelected = this._props.axes.length > 1 ? this._props.axes[0] == col || this._props.axes[1] == col : this._props.axes.length > 0 ? this._props.axes[0] == col : false; return ( <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}> - <div className="tableBox-cell">{this.props.records[rowId][col]}</div> + <div className="tableBox-cell">{this._props.records[rowId][col]}</div> </td> ); })} diff --git a/src/client/views/nodes/MapBox/AnimationUtility.ts b/src/client/views/nodes/MapBox/AnimationUtility.ts index 256acbf13..13ce5b7e2 100644 --- a/src/client/views/nodes/MapBox/AnimationUtility.ts +++ b/src/client/views/nodes/MapBox/AnimationUtility.ts @@ -1,13 +1,13 @@ -import mapboxgl from "mapbox-gl"; -import { MercatorCoordinate } from "mapbox-gl"; -import { MapRef } from "react-map-gl"; +import mapboxgl from 'mapbox-gl'; +import { MercatorCoordinate } from 'mapbox-gl'; +import { MapRef } from 'react-map-gl'; import * as React from 'react'; -import * as d3 from "d3"; +import * as d3 from 'd3'; import * as turf from '@turf/turf'; -import { Position } from "@turf/turf"; -import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from "geojson"; -import { observer } from "mobx-react"; -import { action, computed, observable } from "mobx"; +import { Position } from '@turf/turf'; +import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; +import { observer } from 'mobx-react'; +import { action, computed, observable } from 'mobx'; export enum AnimationStatus { START = 'start', @@ -21,37 +21,35 @@ export enum AnimationSpeed { FAST = '3x', } -@observer export class AnimationUtility { - private SMOOTH_FACTOR = 0.95 - private ROUTE_COORDINATES: Position[] = []; + private SMOOTH_FACTOR = 0.95; + private ROUTE_COORDINATES: Position[] = []; private PATH: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>; private FLY_IN_START_PITCH = 40; - private FIRST_LNG_LAT: {lng: number, lat: number}; + private FIRST_LNG_LAT: { lng: number; lat: number }; private START_ALTITUDE = 3_000_000; @observable private isStreetViewAnimation: boolean; @observable private animationSpeed: AnimationSpeed; - - private previousLngLat: {lng: number, lat: number}; + private previousLngLat: { lng: number; lat: number }; private currentStreetViewBearing: number = 0; @computed get flyInEndBearing() { - return this.isStreetViewAnimation ? - this.calculateBearing( - { - lng: this.ROUTE_COORDINATES[0][0], - lat: this.ROUTE_COORDINATES[0][1] - }, - { - lng: this.ROUTE_COORDINATES[1][0], - lat: this.ROUTE_COORDINATES[1][1] - } - ) + return this.isStreetViewAnimation + ? this.calculateBearing( + { + lng: this.ROUTE_COORDINATES[0][0], + lat: this.ROUTE_COORDINATES[0][1], + }, + { + lng: this.ROUTE_COORDINATES[1][0], + lat: this.ROUTE_COORDINATES[1][1], + } + ) : -20; - } + } @computed get flyInStartBearing() { return Math.max(0, Math.min(this.flyInEndBearing + 20, 360)); // between 0 and 360 @@ -80,8 +78,8 @@ export class AnimationUtility { @computed get animationDuration(): number { return 20_000; - // compute path distance for a standard speed - // get animation speed + // compute path distance for a standard speed + // get animation speed // get isStreetViewAnimation (should be slower if so) } @@ -90,19 +88,13 @@ export class AnimationUtility { // calculate new flyToDuration and animationDuration this.animationSpeed = speed; } - + @action public updateIsStreetViewAnimation(isStreetViewAnimation: boolean) { this.isStreetViewAnimation = isStreetViewAnimation; } - - constructor( - firstLngLat: {lng: number, lat: number}, - routeCoordinates: Position[], - isStreetViewAnimation: boolean, - animationSpeed: AnimationSpeed - ) { + constructor(firstLngLat: { lng: number; lat: number }, routeCoordinates: Position[], isStreetViewAnimation: boolean, animationSpeed: AnimationSpeed) { this.FIRST_LNG_LAT = firstLngLat; this.previousLngLat = firstLngLat; this.ROUTE_COORDINATES = routeCoordinates; @@ -111,11 +103,11 @@ export class AnimationUtility { const bearing = this.calculateBearing( { lng: routeCoordinates[0][0], - lat: routeCoordinates[0][1] + lat: routeCoordinates[0][1], }, { lng: routeCoordinates[1][0], - lat: routeCoordinates[1][1] + lat: routeCoordinates[1][1], } ); this.currentStreetViewBearing = bearing; @@ -137,293 +129,245 @@ export class AnimationUtility { currentAnimationPhase, updateAnimationPhase, updateFrameId, - }: { - map: MapRef, + }: { + map: MapRef; // path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>, // startBearing: number, // startAltitude: number, // pitch: number, - currentAnimationPhase: number, - updateAnimationPhase: ( - newAnimationPhase: number, - ) => void, + currentAnimationPhase: number; + updateAnimationPhase: (newAnimationPhase: number) => void; updateFrameId: (newFrameId: number) => void; + }) => { + return new Promise<void>(async resolve => { + const pathDistance = turf.lineDistance(this.PATH); + console.log('PATH DISTANCE: ', pathDistance); + let startTime: number | null = null; + + const frame = async (currentTime: number) => { + if (!startTime) startTime = currentTime; + const elapsedSinceLastFrame = currentTime - startTime; + const phaseIncrement = elapsedSinceLastFrame / this.animationDuration; + const animationPhase = currentAnimationPhase + phaseIncrement; + + // when the duration is complete, resolve the promise and stop iterating + if (animationPhase > 1) { + resolve(); + return; + } - }) => { - return new Promise<void>(async (resolve) => { - const pathDistance = turf.lineDistance(this.PATH); - console.log("PATH DISTANCE: ", pathDistance); - let startTime: number | null = null; - - const frame = async (currentTime: number) => { - if (!startTime) startTime = currentTime; - const elapsedSinceLastFrame = currentTime - startTime; - const phaseIncrement = elapsedSinceLastFrame / this.animationDuration; - const animationPhase = currentAnimationPhase + phaseIncrement; - - // when the duration is complete, resolve the promise and stop iterating - if (animationPhase > 1) { - resolve(); - return; - } + // calculate the distance along the path based on the animationPhase + const alongPath = turf.along(this.PATH, pathDistance * animationPhase).geometry.coordinates; + + const lngLat = { + lng: alongPath[0], + lat: alongPath[1], + }; + + let bearing: number; + if (this.isStreetViewAnimation) { + bearing = this.lerp( + this.currentStreetViewBearing, + this.calculateBearing(this.previousLngLat, lngLat), + 0.028 // Adjust the transition speed as needed + ); + this.currentStreetViewBearing = bearing; + // bearing = this.calculateBearing(this.previousLngLat, lngLat); // TODO: Calculate actual bearing + } else { + // slowly rotate the map at a constant rate + bearing = this.flyInEndBearing - animationPhase * 200.0; + // bearing = startBearing - animationPhase * 200.0; + } + + this.previousLngLat = lngLat; + + updateAnimationPhase(animationPhase); + + // compute corrected camera ground position, so that he leading edge of the path is in view + var correctedPosition = this.computeCameraPosition( + this.isStreetViewAnimation, + this.flyInEndPitch, + bearing, + lngLat, + this.flyInEndAltitude, + true // smooth + ); + + // set the pitch and bearing of the camera + const camera = map.getFreeCameraOptions(); + camera.setPitchBearing(this.flyInEndPitch, bearing); + + console.log('Corrected pos: ', correctedPosition); + console.log('Start altitude: ', this.flyInEndAltitude); + // set the position and altitude of the camera + camera.position = MercatorCoordinate.fromLngLat(correctedPosition, this.flyInEndAltitude); + + // apply the new camera options + map.setFreeCameraOptions(camera); - - // calculate the distance along the path based on the animationPhase - const alongPath = turf.along(this.PATH, pathDistance * animationPhase).geometry - .coordinates; - - const lngLat = { - lng: alongPath[0], - lat: alongPath[1], + // repeat! + const innerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(innerFrameId); }; - - let bearing: number; - if (this.isStreetViewAnimation){ - bearing = this.lerp( - this.currentStreetViewBearing, - this.calculateBearing(this.previousLngLat, lngLat), - 0.028 // Adjust the transition speed as needed - ); - this.currentStreetViewBearing = bearing; - // bearing = this.calculateBearing(this.previousLngLat, lngLat); // TODO: Calculate actual bearing - } else { - // slowly rotate the map at a constant rate - bearing = this.flyInEndBearing - animationPhase * 200.0; - // bearing = startBearing - animationPhase * 200.0; - } - this.previousLngLat = lngLat; - - updateAnimationPhase(animationPhase); - - // compute corrected camera ground position, so that he leading edge of the path is in view - var correctedPosition = this.computeCameraPosition( - this.isStreetViewAnimation, - this.flyInEndPitch, - bearing, - lngLat, - this.flyInEndAltitude, - true // smooth - ); - - // set the pitch and bearing of the camera - const camera = map.getFreeCameraOptions(); - camera.setPitchBearing(this.flyInEndPitch, bearing); - - console.log("Corrected pos: ", correctedPosition); - console.log("Start altitude: ", this.flyInEndAltitude); - // set the position and altitude of the camera - camera.position = MercatorCoordinate.fromLngLat( - correctedPosition, - this.flyInEndAltitude - ); - - - // apply the new camera options - map.setFreeCameraOptions(camera); - - // repeat! - const innerFrameId = await window.requestAnimationFrame(frame); - updateFrameId(innerFrameId); - }; - - const outerFrameId = await window.requestAnimationFrame(frame); - updateFrameId(outerFrameId); + const outerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(outerFrameId); }); - }; + }; - public flyInAndRotate = async ({ - map, - updateFrameId - }: - { - map: MapRef, - updateFrameId: (newFrameId: number) => void - } - ) => { - return new Promise<{bearing: number, altitude: number}>(async (resolve) => { - let start: number | null; - - var currentAltitude; - var currentBearing; - var currentPitch; - - // the animation frame will run as many times as necessary until the duration has been reached - const frame = async (time: number) => { - if (!start) { - start = time; - } - - // otherwise, use the current time to determine how far along in the duration we are - let animationPhase = (time - start) / this.flyToDuration; - - // because the phase calculation is imprecise, the final zoom can vary - // if it ended up greater than 1, set it to 1 so that we get the exact endAltitude that was requested - if (animationPhase > 1) { - animationPhase = 1; - } - - currentAltitude = this.START_ALTITUDE + (this.flyInEndAltitude - this.START_ALTITUDE) * d3.easeCubicOut(animationPhase) - // rotate the camera between startBearing and endBearing - currentBearing = this.flyInStartBearing + (this.flyInEndBearing - this.flyInStartBearing) * d3.easeCubicOut(animationPhase) - - currentPitch = this.FLY_IN_START_PITCH + (this.flyInEndPitch - this.FLY_IN_START_PITCH) * d3.easeCubicOut(animationPhase) - - // compute corrected camera ground position, so the start of the path is always in view - var correctedPosition = this.computeCameraPosition( - false, - currentPitch, - currentBearing, - this.FIRST_LNG_LAT, - currentAltitude - ); - - // set the pitch and bearing of the camera - const camera = map.getFreeCameraOptions(); - camera.setPitchBearing(currentPitch, currentBearing); - - // set the position and altitude of the camera - camera.position = MercatorCoordinate.fromLngLat( - correctedPosition, - currentAltitude - ); - - // apply the new camera options - map.setFreeCameraOptions(camera); - - // when the animationPhase is done, resolve the promise so the parent function can move on to the next step in the sequence - if (animationPhase === 1) { - resolve({ - bearing: currentBearing, - altitude: currentAltitude, - }); - - // return so there are no further iterations of this frame - return; - } - - const innerFrameId = await window.requestAnimationFrame(frame); - updateFrameId(innerFrameId); - - }; - - const outerFrameId = await window.requestAnimationFrame(frame); - updateFrameId(outerFrameId); + public flyInAndRotate = async ({ map, updateFrameId }: { map: MapRef; updateFrameId: (newFrameId: number) => void }) => { + return new Promise<{ bearing: number; altitude: number }>(async resolve => { + let start: number | null; + + var currentAltitude; + var currentBearing; + var currentPitch; + + // the animation frame will run as many times as necessary until the duration has been reached + const frame = async (time: number) => { + if (!start) { + start = time; + } + + // otherwise, use the current time to determine how far along in the duration we are + let animationPhase = (time - start) / this.flyToDuration; + + // because the phase calculation is imprecise, the final zoom can vary + // if it ended up greater than 1, set it to 1 so that we get the exact endAltitude that was requested + if (animationPhase > 1) { + animationPhase = 1; + } + + currentAltitude = this.START_ALTITUDE + (this.flyInEndAltitude - this.START_ALTITUDE) * d3.easeCubicOut(animationPhase); + // rotate the camera between startBearing and endBearing + currentBearing = this.flyInStartBearing + (this.flyInEndBearing - this.flyInStartBearing) * d3.easeCubicOut(animationPhase); + + currentPitch = this.FLY_IN_START_PITCH + (this.flyInEndPitch - this.FLY_IN_START_PITCH) * d3.easeCubicOut(animationPhase); + + // compute corrected camera ground position, so the start of the path is always in view + var correctedPosition = this.computeCameraPosition(false, currentPitch, currentBearing, this.FIRST_LNG_LAT, currentAltitude); + + // set the pitch and bearing of the camera + const camera = map.getFreeCameraOptions(); + camera.setPitchBearing(currentPitch, currentBearing); + + // set the position and altitude of the camera + camera.position = MercatorCoordinate.fromLngLat(correctedPosition, currentAltitude); + + // apply the new camera options + map.setFreeCameraOptions(camera); + + // when the animationPhase is done, resolve the promise so the parent function can move on to the next step in the sequence + if (animationPhase === 1) { + resolve({ + bearing: currentBearing, + altitude: currentAltitude, + }); + + // return so there are no further iterations of this frame + return; + } + + const innerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(innerFrameId); + }; + + const outerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(outerFrameId); }); - }; + }; - previousCameraPosition: {lng: number, lat: number} | null = null; + previousCameraPosition: { lng: number; lat: number } | null = null; - lerp = (start: number, end: number, amt: number) => { + lerp = (start: number, end: number, amt: number) => { return (1 - amt) * start + amt * end; - } - - computeCameraPosition = ( - isStreetViewAnimation: boolean, - pitch: number, - bearing: number, - targetPosition: {lng: number, lat: number}, - altitude: number, - smooth = false - ) => { + }; + + computeCameraPosition = (isStreetViewAnimation: boolean, pitch: number, bearing: number, targetPosition: { lng: number; lat: number }, altitude: number, smooth = false) => { const bearingInRadian = (bearing * Math.PI) / 180; - const pitchInRadian = ((90 - pitch)* Math.PI) / 180; + const pitchInRadian = ((90 - pitch) * Math.PI) / 180; let correctedLng = targetPosition.lng; let correctedLat = targetPosition.lat; if (!isStreetViewAnimation) { - const lngDiff = - ((altitude / Math.tan(pitchInRadian)) * - Math.sin(-bearingInRadian)) / - 70000; // ~70km/degree longitude - const latDiff = - ((altitude / Math.tan(pitchInRadian)) * - Math.cos(-bearingInRadian)) / - 110000 // 110km/degree latitude + const lngDiff = ((altitude / Math.tan(pitchInRadian)) * Math.sin(-bearingInRadian)) / 70000; // ~70km/degree longitude + const latDiff = ((altitude / Math.tan(pitchInRadian)) * Math.cos(-bearingInRadian)) / 110000; // 110km/degree latitude correctedLng = targetPosition.lng + lngDiff; correctedLat = targetPosition.lat - latDiff; - } - + const newCameraPosition = { - lng: correctedLng, - lat: correctedLat + lng: correctedLng, + lat: correctedLat, }; - + if (smooth) { - if (this.previousCameraPosition) { - newCameraPosition.lng = this.lerp(newCameraPosition.lng, this.previousCameraPosition.lng, this.SMOOTH_FACTOR); - newCameraPosition.lat = this.lerp(newCameraPosition.lat, this.previousCameraPosition.lat, this.SMOOTH_FACTOR); - } + if (this.previousCameraPosition) { + newCameraPosition.lng = this.lerp(newCameraPosition.lng, this.previousCameraPosition.lng, this.SMOOTH_FACTOR); + newCameraPosition.lat = this.lerp(newCameraPosition.lat, this.previousCameraPosition.lat, this.SMOOTH_FACTOR); + } } - - this.previousCameraPosition = newCameraPosition - + + this.previousCameraPosition = newCameraPosition; + return newCameraPosition; - }; - - public static createGeoJSONCircle = (center: number[], radiusInKm: number, points = 64): Feature<Geometry, GeoJsonProperties>=> { + }; + + public static createGeoJSONCircle = (center: number[], radiusInKm: number, points = 64): Feature<Geometry, GeoJsonProperties> => { const coords = { - latitude: center[1], - longitude: center[0], + latitude: center[1], + longitude: center[0], }; const km = radiusInKm; const ret = []; - const distanceX = km / (111.320 * Math.cos((coords.latitude * Math.PI) / 180)); + const distanceX = km / (111.32 * Math.cos((coords.latitude * Math.PI) / 180)); const distanceY = km / 110.574; let theta; let x; let y; for (let i = 0; i < points; i += 1) { - theta = (i / points) * (2 * Math.PI); - x = distanceX * Math.cos(theta); - y = distanceY * Math.sin(theta); - ret.push([coords.longitude + x, coords.latitude + y]); + theta = (i / points) * (2 * Math.PI); + x = distanceX * Math.cos(theta); + y = distanceY * Math.sin(theta); + ret.push([coords.longitude + x, coords.latitude + y]); } ret.push(ret[0]); return { - type: 'Feature', - geometry: { - type: 'Polygon', - coordinates: [ret], - }, - properties: {} + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ret], + }, + properties: {}, }; - } + }; - private calculateBearing( - from: { lng: number; lat: number }, - to: { lng: number; lat: number } - ): number { + private calculateBearing(from: { lng: number; lat: number }, to: { lng: number; lat: number }): number { const lon1 = from.lng; const lat1 = from.lat; const lon2 = to.lng; const lat2 = to.lat; - + const lon1Rad = (lon1 * Math.PI) / 180; const lon2Rad = (lon2 * Math.PI) / 180; const lat1Rad = (lat1 * Math.PI) / 180; const lat2Rad = (lat2 * Math.PI) / 180; - + const y = Math.sin(lon2Rad - lon1Rad) * Math.cos(lat2Rad); - const x = - Math.cos(lat1Rad) * Math.sin(lat2Rad) - - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(lon2Rad - lon1Rad); - - let bearing = Math.atan2(y,x); - + const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(lon2Rad - lon1Rad); + + let bearing = Math.atan2(y, x); + // Convert bearing from radians to degrees bearing = (bearing * 180) / Math.PI; - + // Ensure the bearing is within [0, 360) if (bearing < 0) { - bearing += 360; + bearing += 360; } - - return bearing; - } - -}
\ No newline at end of file + return bearing; + } +} diff --git a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx index bf4028f01..f9607becf 100644 --- a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx @@ -1,15 +1,15 @@ -import React = require('react'); -import { observer } from "mobx-react"; -import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu"; -import { IReactionDisposer, ObservableMap, reaction } from "mobx"; -import { Doc, Opt } from "../../../../fields/Doc"; -import { returnFalse, unimplementedFunction } from "../../../../Utils"; -import { NumCast, StrCast } from "../../../../fields/Types"; -import { SelectionManager } from "../../../util/SelectionManager"; -import { IconButton } from "browndash-components"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { SettingsManager } from "../../../util/SettingsManager"; -import { IconLookup, faAdd, faCalendarDays, faRoute } from "@fortawesome/free-solid-svg-icons"; +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { IReactionDisposer, ObservableMap, reaction } from 'mobx'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { returnFalse, unimplementedFunction } from '../../../../Utils'; +import { NumCast, StrCast } from '../../../../fields/Types'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { IconButton } from 'browndash-components'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { SettingsManager } from '../../../util/SettingsManager'; +import { IconLookup, faAdd, faCalendarDays, faRoute } from '@fortawesome/free-solid-svg-icons'; @observer export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -32,9 +32,9 @@ export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { private title: string | undefined = undefined; - public setPinDoc(pinDoc: Doc){ - this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`) ; - console.log("Title: ", this.title) + public setPinDoc(pinDoc: Doc) { + this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`); + console.log('Title: ', this.title); } public get Active() { @@ -54,7 +54,7 @@ export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views().slice(), + () => SelectionManager.Views.slice(), sel => DirectionsAnchorMenu.Instance.fadeOut(true) ); } @@ -95,43 +95,28 @@ export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { render() { const buttons = ( - <div className='directions-menu-buttons' style={{display: 'flex'}}> + <div className="directions-menu-buttons" style={{ display: 'flex' }}> <IconButton tooltip="Add route" // onPointerDown={this.Delete} icon={<FontAwesomeIcon icon={faAdd as IconLookup} />} color={SettingsManager.userColor} /> - - - <IconButton - tooltip='Animate route' - onPointerDown={this.Delete} /**TODO: fix */ - icon={<FontAwesomeIcon icon={faRoute as IconLookup}/>} - color={SettingsManager.userColor} - /> - <IconButton - tooltip='Add to calendar' - onPointerDown={this.Delete} /**TODO: fix */ - icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup}/>} - color={SettingsManager.userColor} - /> + + <IconButton tooltip="Animate route" onPointerDown={this.Delete} /**TODO: fix */ icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} color={SettingsManager.userColor} /> + <IconButton tooltip="Add to calendar" onPointerDown={this.Delete} /**TODO: fix */ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} /> </div> - ) + ); return this.getElement( - <div ref={DirectionsAnchorMenu.top} style={{ height: 'max-content' , width: '100%', display: 'flex', flexDirection: 'column' }}> + <div ref={DirectionsAnchorMenu.top} style={{ height: 'max-content', width: '100%', display: 'flex', flexDirection: 'column' }}> <div>{this.title}</div> - <div className='direction-inputs' style={{display: 'flex', flexDirection: 'column'}}> - <input - placeholder="Origin" - /> - <input - placeholder="Destination" - /> + <div className="direction-inputs" style={{ display: 'flex', flexDirection: 'column' }}> + <input placeholder="Origin" /> + <input placeholder="Destination" /> </div> {buttons} </div> - ) + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx index a6182991d..6a14427c0 100644 --- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx +++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx @@ -43,7 +43,7 @@ import { MapProvider, Map as MapboxMap } from 'react-map-gl'; * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps */ -const mapboxApiKey = "pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNsbnc2eHJpbTA1ZTUyam85aGx4Z2FhbGwifQ.2Kqw9mk-9wAAg9kmHmKzcg"; +const mapboxApiKey = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNsbnc2eHJpbTA1ZTUyam85aGx4Z2FhbGwifQ.2Kqw9mk-9wAAg9kmHmKzcg'; const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey> /** @@ -198,7 +198,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata title="Toggle Sidebar" style={{ display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5, + top: StrCast(this.Document._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> @@ -230,8 +230,8 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata }); const targetCreator = (annotationOn: Doc | undefined) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow'); - FormattedTextBox.SelectOnLoad = target[Id]; + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow'); + FormattedTextBox.SetSelectOnLoad(target); return target; }; const docView = this.props.DocumentView?.(); @@ -324,7 +324,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata }; @observable - bingSearchBarContents: any = this.rootDoc.map; // For Bing Maps: The contents of the Bing search bar (string) + bingSearchBarContents: any = this.Document.map; // For Bing Maps: The contents of the Bing search bar (string) geoDataRequestOptions = { entityType: 'PopulatedPlace', @@ -357,9 +357,9 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata deselectPin = () => { if (this.selectedPin) { // Removes filter - Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); - Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); - Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove'); + Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); const temp = this.selectedPin; if (!this._unmounting) { @@ -372,7 +372,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata } this.map_docToPinMap.set(temp, newpin); this.selectedPin = undefined; - this.bingSearchBarContents = this.rootDoc.map; + this.bingSearchBarContents = this.Document.map; } }; @@ -389,9 +389,9 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata this.selectedPin = pinDoc; this.bingSearchBarContents = pinDoc.map; - // Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'match'); - // Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'match'); - Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPin)}`, 'check'); + // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match'); + // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPin)}`, 'check'); this.recolorPin(this.selectedPin, 'green'); @@ -460,8 +460,8 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps, existingPin?: Doc) => { /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER const anchor = Docs.Create.ConfigDocument({ - title: 'MapAnchor:' + this.rootDoc.title, - text: StrCast(this.selectedPin?.map) || StrCast(this.rootDoc.map) || 'map location', + title: 'MapAnchor:' + this.Document.title, + text: StrCast(this.selectedPin?.map) || StrCast(this.Document.map) || 'map location', config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude), config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude), config_map_zoom: NumCast(this.dataDoc.map_zoom), @@ -469,15 +469,15 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata config_map: StrCast((existingPin ?? this.selectedPin)?.map) || StrCast(this.dataDoc.map), layout_unrendered: true, mapPin: existingPin ?? this.selectedPin, - annotationOn: this.rootDoc, + annotationOn: this.Document, }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; addAsAnnotation && this.addDocument(anchor); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document); return anchor; } - return this.rootDoc; + return this.Document; }; map_docToPinMap = new Map<Doc, any>(); @@ -524,9 +524,9 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata deleteSelectedPin = undoable(() => { if (this.selectedPin) { // Removes filter - Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); - Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); - Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove'); + Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); this.removePushpin(this.selectedPin); } @@ -614,7 +614,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change')); this._disposers.mapLocation = reaction( - () => this.rootDoc.map, + () => this.Document.map, mapLoc => (this.bingSearchBarContents = mapLoc), { fireImmediately: true } ); @@ -639,7 +639,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata ); this._disposers.location = reaction( - () => ({ lat: this.rootDoc.latitude, lng: this.rootDoc.longitude, zoom: this.rootDoc.map_zoom, mapType: this.rootDoc.map_type }), + () => ({ lat: this.Document.latitude, lng: this.Document.longitude, zoom: this.Document.map_zoom, mapType: this.Document.map_type }), locationObject => { // if (this._bingMap.current) try { @@ -690,7 +690,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata } }, e => { - const createPin = () => this.createPushpin(this.rootDoc.latitude, this.rootDoc.longitude, this.rootDoc.map); + const createPin = () => this.createPushpin(this.Document.latitude, this.Document.longitude, this.Document.map); if (this.bingSearchBarContents) { this.bingSearch().then(createPin); } else createPin(); @@ -706,7 +706,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata render() { // bcz: no idea what's going on here, but bings maps have some kind of bug // such that we need to delay rendering a second map on startup until the first map is rendered. - this.rootDoc[DocCss]; + this.Document[DocCss]; if (MapBoxContainer._rerenderDelay) { // prettier-ignore this._rerenderTimeout = this._rerenderTimeout ?? @@ -715,7 +715,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata MapBoxContainer._rerenderDelay = 0; } this._rerenderTimeout = undefined; - this.rootDoc[DocCss] = this.rootDoc[DocCss] + 1; + this.Document[DocCss] = this.Document[DocCss] + 1; }), MapBoxContainer._rerenderDelay); return null; } @@ -732,7 +732,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> {renderAnnotations(this.opaqueFilter)} - {SnappingManager.GetIsDragging() ? null : renderAnnotations()} + {SnappingManager.IsDragging ? null : renderAnnotations()} <div className="mapBox-searchbar"> <EditableText @@ -758,15 +758,10 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata </div> </div> <MapProvider> - <MapboxMap - id="mabox-map" - mapStyle={`mapbox://styles/mapbox/streets-v9`} - mapboxAccessToken={mapboxApiKey} - /> + <MapboxMap id="mabox-map" mapStyle={`mapbox://styles/mapbox/streets-v9`} mapboxAccessToken={mapboxApiKey} /> </MapProvider> - - -{/* + + {/* <BingMapsReact onMapReady={this.bingMapReady} // bingMapsKey={bingApiKey} @@ -786,7 +781,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata {...this.props} renderDepth={this.props.renderDepth + 1} Document={pushpin} - DataDoc={undefined} PanelWidth={returnOne} PanelHeight={returnOne} NativeWidth={returnOne} @@ -824,7 +818,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotata ref={this._sidebarRef} {...this.props} fieldKey={this.fieldKey} - rootDoc={this.rootDoc} + Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} usePanelWidth={true} |