function _Explorer()

in packages/sanddance-explorer/src/explorer.tsx [157:1493]


function _Explorer(props: Props) {
    class __Explorer extends base.react.Component<Props, State> {
        private layoutDivUnpinned: HTMLElement;
        private layoutDivPinned: HTMLElement;
        private getColorContext: (oldInsight: SandDance.specs.Insight, newInsight: SandDance.specs.Insight) => SandDance.types.ColorContext;
        private historicFilterChange: string;
        private rebaseFilter: boolean;
        private ignoreSelectionChange: boolean;
        private snapshotEditor: SnapshotEditor_Class;
        private scrollSnapshotTimer: number;
        private newViewStateTarget: boolean;

        public viewer: SandDance.Viewer;
        public viewerOptions: Partial<SandDance.types.ViewerOptions>;
        public discardColorContextUpdates: boolean;
        public prefs: Prefs;
        public div: HTMLElement;
        public snapshotThumbWidth: number;

        constructor(props: Props) {
            super(props);
            this.state = {
                calculating: null,
                errors: null,
                autoCompleteDistinctValues: {},
                colorBin: null,
                dataContent: null,
                dataFile: null,
                search: null,
                totalStyle: null,
                facetStyle: 'wrap',
                filter: null,
                filteredData: null,
                specCapabilities: null,
                size: {
                    height: null,
                    width: null
                },
                scheme: null,
                transform: null,
                columns: null,
                chart: 'grid',
                signalValues: null,
                hideAxes: false,
                hideLegend: false,
                sideTabId: SideTabId.ChartType,
                dataScopeId: DataScopeId.AllData,
                selectedItemIndex: {},
                sidebarClosed: false,
                sidebarPinned: true,
                view: props.initialView || '2d',
                snapshots: [],
                selectedSnapshotIndex: -1,
                tooltipExclusions: [],
                positionedColumnMapProps: null,
                note: null,
                historyIndex: -1,
                historyItems: []
            };

            this.state.selectedItemIndex[DataScopeId.AllData] = 0;
            this.state.selectedItemIndex[DataScopeId.FilteredData] = 0;
            this.state.selectedItemIndex[DataScopeId.SelectedData] = 0;

            this.snapshotThumbWidth = snapshotThumbWidth;
            this.discardColorContextUpdates = true;
            this.updateViewerOptions({ ...SandDance.VegaDeckGl.util.clone(SandDance.Viewer.defaultViewerOptions), ...props.viewerOptions });
        }

        public finalize() {
            if (this.viewer) this.viewer.finalize();
        }

        public updateViewerOptions(viewerOptions: Partial<SandDance.types.ViewerOptions>) {
            this.viewerOptions = {
                ...SandDance.VegaDeckGl.util.deepMerge(
                    defaultViewerOptions,
                    this.viewerOptions,
                    viewerOptions
                ),
                tooltipOptions: {
                    exclude: columnName => this.state.tooltipExclusions.indexOf(columnName) >= 0
                },
                onColorContextChange: () => this.manageColorToolbar(),
                onDataFilter: (filter, filteredData) => {
                    const selectedItemIndex = { ...this.state.selectedItemIndex };
                    selectedItemIndex[DataScopeId.FilteredData] = 0;
                    this.changeInsight(
                        { filter },
                        { label: this.historicFilterChange, omit: !this.historicFilterChange }
                    );
                    this.historicFilterChange = null;
                    this.setState({ filteredData, selectedItemIndex });
                    if (this.state.sideTabId === SideTabId.Data && this.state.dataScopeId === DataScopeId.FilteredData) {
                        //make sure item is active
                        requestAnimationFrame(() => filteredData && this.silentActivation(filteredData[0]));
                    }
                    viewerOptions && viewerOptions.onDataFilter && viewerOptions.onDataFilter(filter, filteredData);
                },
                onSelectionChanged: (newSearch, index, selectedData) => {
                    if (this.ignoreSelectionChange) return;
                    const selectedItemIndex = { ...this.state.selectedItemIndex };
                    selectedItemIndex[DataScopeId.SelectedData] = index || 0;
                    let { search } = this.state;
                    const { sideTabId } = this.state;
                    if (newSearch) {
                        search = createInputSearch(newSearch);
                        //} else {
                        //sideTabId = SideTabId.ChartType;
                    }
                    this.setState({ search, selectedItemIndex, sideTabId });
                    viewerOptions && viewerOptions.onSelectionChanged && viewerOptions.onSelectionChanged(newSearch, index, selectedData);
                },
                onAxisClick: (e, search) => {
                    this.toggleableSearch(e, search);
                    viewerOptions && viewerOptions.onAxisClick && viewerOptions.onAxisClick(e, search);
                },
                onLegendHeaderClick: e => {
                    const pos = getPosition(e);
                    const specRole = this.state.specCapabilities && this.state.specCapabilities.roles.filter(r => r.role === 'color')[0];
                    const positionedColumnMapProps: PositionedColumnMapProps = {
                        ...this.getColumnMapBaseProps(),
                        collapseLabel: true,
                        container: this.div,
                        selectedColumnName: this.state.columns['color'],
                        onDismiss: () => { this.setState({ positionedColumnMapProps: null }); },
                        specRole,
                        left: pos.left - this.div.clientLeft,
                        top: pos.top - this.div.clientTop
                    };
                    this.setState({ positionedColumnMapProps });
                },
                onLegendRowClick: (e, legendRow) => {
                    this.toggleableSearch(e, legendRow.search);
                    viewerOptions && viewerOptions.onLegendRowClick && viewerOptions.onLegendRowClick(e, legendRow);
                },
                onError: (errors) => {
                    this.setState({ errors });
                    viewerOptions && viewerOptions.onError && viewerOptions.onError(errors);
                },
                onBeforeCreateLayers,
                getTextColor: o => {
                    if ((o as TextWithSpecRole).specRole) {
                        return SandDance.VegaDeckGl.util.colorFromString((this.viewerOptions.colors as ColorSettings).clickableText);
                    } else if (o.metaData && o.metaData.search) {
                        return SandDance.VegaDeckGl.util.colorFromString((this.viewerOptions.colors as ColorSettings).searchText);
                    } else {
                        return o.color;
                    }
                },
                getTextHighlightAlphaCutoff: () => (this.viewerOptions.colors as ColorSettings).clickableTextHighlightAlphaCutoff,
                getTextHighlightColor: o => {
                    if ((o as TextWithSpecRole).specRole) {
                        return SandDance.VegaDeckGl.util.colorFromString((this.viewerOptions.colors as ColorSettings).clickableTextHighlight);
                    } else if (o.metaData && o.metaData.search) {
                        return SandDance.VegaDeckGl.util.colorFromString((this.viewerOptions.colors as ColorSettings).searchTextHighlight);
                    } else {
                        return [0, 0, 0, 0];
                    }
                },
                onTextClick: (e, text) => {
                    if (e && text) {
                        const pos = getPosition(e);
                        const { specRole } = text as TextWithSpecRole;
                        if (pos && specRole) {
                            const positionedColumnMapProps: PositionedColumnMapProps = {
                                ...this.getColumnMapBaseProps(),
                                collapseLabel: true,
                                container: this.div,
                                selectedColumnName: this.state.columns[specRole.role],
                                onDismiss: () => { this.setState({ positionedColumnMapProps: null }); },
                                specRole,
                                left: pos.left - this.div.clientLeft,
                                top: pos.top - this.div.clientTop
                            };
                            this.setState({ positionedColumnMapProps });
                        } else {
                            this.setState({ positionedColumnMapProps: null });
                        }
                    }
                },
                onNewViewStateTarget: () => this.newViewStateTarget
            };
            if (this.viewer && this.viewer.presenter) {
                const newPresenterStyle = SandDance.util.getPresenterStyle(this.viewerOptions as SandDance.types.ViewerOptions);
                const mergePrenterStyle = { ...this.viewer.presenter.style, ...newPresenterStyle };
                this.viewer.presenter.style = mergePrenterStyle;
                this.viewer.options = SandDance.VegaDeckGl.util.deepMerge(this.viewer.options, this.props.viewerOptions, this.viewerOptions) as SandDance.types.ViewerOptions;
            }
        }

        public signal(signalName: string, signalValue: any, newViewStateTarget?: boolean) {
            switch (signalName) {
                case SandDance.constants.SignalNames.ColorBinCount:
                case SandDance.constants.SignalNames.ColorReverse:
                case SandDance.constants.SignalNames.MarkOpacity:
                    this.discardColorContextUpdates = false;
                    break;
            }
            this.newViewStateTarget = newViewStateTarget;
            this.viewer.vegaViewGl.signal(signalName, signalValue);
            this.viewer.vegaViewGl.runAsync().then(() => {

                //deeply set the state without a state change. This prevents a redraw if re-rendered
                if (this.state.signalValues) {
                    this.state.signalValues[signalName] = signalValue;
                }

                this.discardColorContextUpdates = true;
                this.newViewStateTarget = undefined;
                this.props.onSignalChanged && this.props.onSignalChanged(signalName, signalValue);
            });
        }

        private manageColorToolbar() {
            const canRemap = this.viewer.colorContexts && this.viewer.colorContexts.length > 1;
            applyColorButtons(this.viewer.presenter, !!this.state.columns.color, {
                themePalette: themePalettes[this.props.theme || ''],
                canRemap,
                isRemap: canRemap && this.viewer.currentColorContext > 0,
                colorMapHandler: remap => {
                    this.viewer.currentColorContext = ~~remap;
                    this.viewer.renderSameLayout();
                    this.manageColorToolbar();
                }
            });
        }

        public getInsight() {
            return this.viewer.getInsight();
        }

        public setInsight(historyAction: HistoryAction, newState: Partial<UIState> = {}, partialInsight: Partial<SandDance.specs.Insight> = this.viewer.getInsight(), rebaseFilter = false) {
            const selectedItemIndex = { ...this.state.selectedItemIndex };
            selectedItemIndex[DataScopeId.AllData] = 0;
            selectedItemIndex[DataScopeId.FilteredData] = 0;
            selectedItemIndex[DataScopeId.SelectedData] = 0;
            const historicInsight: Partial<HistoricInsight> = {
                chart: null,
                scheme: null,
                columns: null,
                filter: null,
                rebaseFilter,
                ...partialInsight
            };
            const state: Partial<UIState> = {
                filteredData: null,
                selectedItemIndex,
                search: createInputSearch(historicInsight.filter),
                ...newState
            };
            const changeInsight = () => {
                this.getColorContext = null;
                this.changeInsight(historicInsight, historyAction, state);
            };
            const currentFilter = this.viewer.getInsight().filter;
            if (rebaseFilter && currentFilter && historicInsight.filter) {
                if (SandDance.searchExpression.startsWith(historicInsight.filter, currentFilter)) {
                    changeInsight();
                } else {
                    this.viewer.reset()
                        .then(() => new Promise((resolve, reject) => { setTimeout(resolve, this.viewer.options.transitionDurations.scope); }))
                        .then(changeInsight);
                }
            } else {
                changeInsight();
            }
        }

        private handleReviveSnapshot(snapshot: Snapshot, selectedSnapshotIndex: number) {
            let handled = false;
            if (this.props.onSnapshotClick) {
                this.setState({ selectedSnapshotIndex });
                handled = this.props.onSnapshotClick(snapshot, selectedSnapshotIndex) as boolean;
            }
            if (!handled) {
                this.reviveSnapshot(selectedSnapshotIndex);
            }
        }

        public reviveSnapshot(snapshotOrIndex: Snapshot | number) {
            if (typeof snapshotOrIndex === 'number') {
                const selectedSnapshotIndex = snapshotOrIndex as number;
                const snapshot = this.state.snapshots[selectedSnapshotIndex];
                const newState: Partial<UIState> = { note: snapshot.description, selectedSnapshotIndex };
                if (!this.state.sidebarClosed) {
                    newState.sideTabId = SideTabId.Snapshots;
                    this.scrollSnapshotIntoView(selectedSnapshotIndex);
                }
                this.setInsight({ label: strings.labelHistoryReviveSnapshot }, newState, snapshot.insight, true);
            } else {
                const snapshot = snapshotOrIndex as Snapshot;
                if (snapshot.insight) {
                    this.setInsight({ label: strings.labelHistoryReviveSnapshot }, { note: snapshot.description, selectedSnapshotIndex: -1 }, snapshot.insight, true); //don't navigate to sideTab
                } else {
                    this.setState({ note: snapshot.description, selectedSnapshotIndex: -1 });
                }
            }
        }

        public load(
            data: DataFile | object[],
            getPartialInsight?: (
                columns: SandDance.types.Column[]
            ) => Partial<SandDance.specs.Insight>,
            optionsOrPrefs?: Prefs | Options
        ) {
            this.setState({ historyIndex: -1, historyItems: [] });
            this.changeInsight(
                { columns: null },
                { label: null, omit: true },
                { note: null }
            );
            return new Promise<void>((resolve, reject) => {
                const loadFinal = (dataContent: DataContent) => {
                    let partialInsight: Partial<SandDance.specs.Insight>;
                    this.prefs = (optionsOrPrefs && (optionsOrPrefs as Options).chartPrefs || (optionsOrPrefs as Prefs)) || {};
                    if (getPartialInsight) {
                        partialInsight = getPartialInsight(dataContent.columns);
                        initPrefs(this.prefs, partialInsight);
                    }
                    if (!partialInsight) {
                        //load recommendation
                        let r = new RecommenderSummary(dataContent.columns, dataContent.data);
                        partialInsight = r.recommend();
                    }
                    partialInsight = {
                        facetStyle: 'wrap',
                        filter: null,
                        totalStyle: null,
                        transform: null,
                        ...partialInsight
                    };
                    if (partialInsight.chart === 'barchart') {
                        partialInsight.chart = 'barchartV';
                    }
                    const selectedItemIndex = { ...this.state.selectedItemIndex };
                    const sideTabId = SideTabId.ChartType;
                    selectedItemIndex[DataScopeId.AllData] = 0;
                    selectedItemIndex[DataScopeId.FilteredData] = 0;
                    selectedItemIndex[DataScopeId.SelectedData] = 0;
                    let newState: Partial<State> = {
                        dataFile,
                        dataContent,
                        snapshots: dataContent.snapshots || this.state.snapshots,
                        autoCompleteDistinctValues: {},
                        filteredData: null,
                        tooltipExclusions: (optionsOrPrefs && (optionsOrPrefs as Options).tooltipExclusions) || [],
                        selectedItemIndex,
                        sideTabId,
                        ...partialInsight
                    };
                    this.getColorContext = null;
                    ensureColumnsExist(newState.columns, dataContent.columns, newState.transform);
                    const errors = ensureColumnsPopulated(partialInsight?.chart, partialInsight?.totalStyle, newState.columns, dataContent.columns);
                    newState.errors = errors;
                    //change insight
                    this.changeInsight(
                        partialInsight,
                        { label: strings.labelHistoryInit, insert: true },
                        newState as State
                    );
                    //make sure item is active
                    this.activateDataBrowserItem(sideTabId, this.state.dataScopeId);
                    resolve();
                };
                let dataFile: DataFile;
                if (Array.isArray(data)) {
                    return loadDataArray(data, 'json')
                        .then(result => {
                            dataFile = {
                                type: 'json'
                            };
                            loadFinal(result);
                        })
                        .catch(reject);
                } else {
                    dataFile = data as DataFile;
                    return loadDataFile(dataFile)
                        .then(loadFinal)
                        .catch(reject);
                }
            });
        }

        public changeChartType(chart: SandDance.specs.Chart) {
            const partialInsight = copyPrefToNewState(this.prefs, chart, '*', '*');
            const insight: Partial<HistoricInsight> = { chart, ...partialInsight };
            const columns = SandDance.VegaDeckGl.util.deepMerge({}, partialInsight.columns, this.state.columns);
            const { signalValues } = this.viewer.getInsight();
            insight.signalValues = { ...this.state.signalValues, ...signalValues };
            insight.columns = { ...columns };
            insight.totalStyle = this.state.totalStyle;
            let errors: string[];

            //special case mappings when switching chart type
            if (this.state.chart === 'scatterplot' && (chart === 'barchart' || chart === 'barchartV')) {
                insight.columns = { ...columns, sort: columns.y };
            } else if (this.state.chart === 'scatterplot' && chart === 'barchartH') {
                insight.columns = { ...columns, sort: columns.x };
            } else if (chart === 'treemap') {
                insight.view = '2d';
                if (!columns.size) {
                    //make sure size exists and is numeric
                    let sizeColumn: SandDance.types.Column;
                    //first check prefs
                    if (partialInsight && partialInsight.columns && partialInsight.columns.size) {
                        const prefSizeColumn = this.state.dataContent.columns.filter(c => c.name === partialInsight.columns.size)[0];
                        if (prefSizeColumn && prefSizeColumn.quantitative) {
                            sizeColumn = prefSizeColumn;
                        }
                    }
                    if (!sizeColumn) {
                        sizeColumn = getTreemapColumn(this.state.dataContent.columns);
                    }
                    if (!sizeColumn) {
                        //error - no numeric columns
                        errors = [strings.errorColumnMustBeNumeric];
                    } else {
                        insight.columns = { ...columns, size: sizeColumn.name };
                    }
                }
            } else if (chart === 'stacks') {
                insight.view = '3d';
            }

            ensureColumnsExist(insight.columns, this.state.dataContent.columns, this.state.transform);
            errors = ensureColumnsPopulated(chart, insight.totalStyle, insight.columns, this.state.dataContent.columns);

            this.calculate(() => {
                this.changeInsight(
                    insight,
                    { label: strings.labelHistoryChangeChartType(chartLabel(chart)) },
                    errors ? { errors } : null
                );
            });
        }

        public calculate(calculating: () => any) {
            this.setState({ calculating });
        }

        public changeView(view: SandDance.types.View) {
            this.changeInsight(
                { view },
                { label: view === '2d' ? strings.labelViewType2d : strings.labelViewType3d }
            );
        }

        //state members which change the insight
        public changeInsight(partialInsight: Partial<SandDance.specs.Insight>, historyAction: HistoryAction, additionalUIState?: Partial<UIState>) {
            if (partialInsight.chart === 'barchart') {
                partialInsight.chart = 'barchartV';
            }
            this.addHistory(partialInsight, historyAction, additionalUIState);
        }

        public addHistory(historicInsight: Partial<HistoricInsight>, historyAction: HistoryAction, additionalUIState?: Partial<UIState>) {

            const setCleanState = (newState: State) => {
                const cleanState = { ...newState, ...additionalUIState };
                if (!cleanState.note) {
                    cleanState.note = null;
                }
                delete cleanState.rebaseFilter;

                if (this.viewer) {
                    const { signalValues } = this.viewer.getInsight();
                    cleanState.signalValues = { ...this.state.signalValues, ...signalValues, ...cleanState.signalValues };
                }

                this.setState(cleanState);
            };

            if (historyAction.omit) {
                setCleanState(historicInsight as State);
                return;
            }
            const historyItems = this.state.historyItems.slice(0, this.state.historyIndex + 1);
            const historyIndex = historyItems.length;
            historyItems.push({ label: historyAction.label, historicInsight });
            if (historyAction.insert) {
                setCleanState({ historyIndex, historyItems } as State);
            } else {
                setCleanState({ ...historicInsight, historyIndex, historyItems } as State);
            }
        }

        private replay(index: number) {
            let filter: SandDance.searchExpression.Search = null;
            let historicInsight: Partial<HistoricInsight> = {};
            for (let i = 0; i < index + 1; i++) {
                const historyItem = this.state.historyItems[i];
                if (historyItem) {
                    if (historyItem.historicInsight.filter === null) {
                        filter = null;
                    } else if (historyItem.historicInsight.rebaseFilter) {
                        filter = historyItem.historicInsight.filter;
                    } else if (historyItem.historicInsight.filter) {
                        filter = SandDance.searchExpression.narrow(filter, historyItem.historicInsight.filter);
                    }
                    historicInsight = { ...historicInsight, ...historyItem.historicInsight };
                }
            }
            return { ...historicInsight, filter };
        }

        public undo() {
            const historyIndex = this.state.historyIndex - 1;
            if (historyIndex < 0) return;
            const newState = this.replay(historyIndex);
            this.rebaseFilter = true;
            this.setState({ ...newState as State, historyIndex });
        }

        public redo(historyIndex = this.state.historyIndex + 1) {
            if (historyIndex >= this.state.historyItems.length) return;
            const newState = this.replay(historyIndex);
            this.rebaseFilter = true;
            this.setState({ ...newState as State, historyIndex });
        }

        public changespecCapabilities(
            specCapabilities: SandDance.specs.SpecCapabilities
        ) {
            this.setState({ specCapabilities });
        }

        public changeColumnMapping(role: SandDance.specs.InsightColumnRoles, column: SandDance.types.Column, options?: ChangeColumnMappingOptions) {
            const columns = { ...this.state.columns };
            const label = column ? strings.labelHistoryMapColumn(role) : strings.labelHistoryUnMapColumn(role);
            const final = () => {
                const partialInsight: Partial<SandDance.specs.Insight> = { columns, totalStyle: options ? options.totalStyle : this.state.totalStyle };
                const errors = ensureColumnsPopulated(this.state.chart, partialInsight.totalStyle, partialInsight.columns, this.state.dataContent.columns);

                columns[role] = column && column.name;
                this.changeInsight(
                    partialInsight,
                    { label },
                    errors ? { errors } : null
                );
            };
            const _changeInsight = (newInsight: Partial<HistoricInsight>, columnUpdate: SandDance.specs.InsightColumns, historyAction: HistoryAction) => {
                newInsight.columns = SandDance.VegaDeckGl.util.deepMerge(
                    {},
                    columns,
                    columnUpdate
                );
                savePref(this.prefs, this.state.chart, '*', '*', { columns: columnUpdate });
                this.changeInsight(newInsight, historyAction);
            };
            if (column) {
                let columnUpdate: SandDance.specs.InsightColumns;
                switch (role) {
                    case 'facet': {
                        copyPrefToNewState(this.prefs, this.state.chart, 'facet', column.name);
                        const historicInsight: Partial<HistoricInsight> = { columns, facetStyle: options ? options.facetStyle : this.state.facetStyle };
                        columnUpdate = { facet: column.name };
                        _changeInsight(historicInsight, columnUpdate, { label });
                        break;
                    }
                    case 'color': {
                        let calculating: () => void = null;
                        let historicInsight: Partial<HistoricInsight> = { scheme: options && options.scheme, columns, colorBin: this.state.colorBin };
                        if (!historicInsight.scheme) {
                            copyPrefToNewState(this.prefs, this.state.chart, 'color', column.name);
                        }
                        if (!historicInsight.scheme) {
                            historicInsight.scheme = bestColorScheme(column, null, this.state.scheme);
                        }
                        if (!column.stats.hasColorData) {
                            historicInsight.directColor = false;
                            if (this.state.directColor !== historicInsight.directColor) {
                                calculating = () => this._resize();
                            }
                        }
                        if (this.state.columns && this.state.columns.color && this.state.columns.color !== column.name) {
                            const currColorColumn = this.state.dataContent.columns.filter(c => c.name === this.state.columns.color)[0];
                            if (column.isColorData != currColorColumn.isColorData) {
                                calculating = () => this._resize();
                            }
                        }
                        this.ignoreSelectionChange = true;
                        this.viewer.deselect().then(() => {
                            this.ignoreSelectionChange = false;
                            //allow deselection to render
                            requestAnimationFrame(() => {
                                columnUpdate = { color: column.name };
                                this.getColorContext = null;
                                this.setState({ calculating });
                                _changeInsight(historicInsight, columnUpdate, { label });
                            });
                        });
                        break;
                    }
                    case 'x': {
                        copyPrefToNewState(this.prefs, this.state.chart, 'x', column.name);
                        const historicInsight: Partial<HistoricInsight> = { columns };
                        columnUpdate = { x: column.name };
                        _changeInsight(historicInsight, columnUpdate, { label });
                        break;
                    }
                    case 'size': {
                        copyPrefToNewState(this.prefs, this.state.chart, 'size', column.name);
                        const historicInsight: Partial<HistoricInsight> = { totalStyle: options ? options.totalStyle : this.state.totalStyle };
                        columnUpdate = { size: column.name };
                        _changeInsight(historicInsight, columnUpdate, { label });
                        break;
                    }
                    default: {
                        final();
                        break;
                    }
                }
            } else {
                switch (role) {
                    case 'facet': {
                        columns.facet = null;
                        columns.facetV = null;
                        this.changeInsight(
                            { columns, facetStyle: 'wrap' },
                            { label }
                        );
                        break;
                    }
                    default: {
                        final();
                        break;
                    }
                }
            }
        }

        private setSideTabId(sideTabId: SideTabId, dataScopeId?: DataScopeId) {
            if (sideTabId === SideTabId.Data && dataScopeId == null) {
                //choose most relevant DataScopeId
                dataScopeId = this.getBestDataScopeId();
            }
            if (dataScopeId == null) {
                dataScopeId = this.state.dataScopeId;
            }
            this.setState({ sideTabId, dataScopeId, sidebarClosed: false });
            this.activateDataBrowserItem(sideTabId, dataScopeId);
        }

        private getBestDataScopeId() {
            let dataScopeId: DataScopeId;
            const selectionState: SandDance.types.SelectionState = this.viewer && this.viewer.getSelection();
            if (selectionState && selectionState.selectedData && selectionState.selectedData.length) {
                dataScopeId = DataScopeId.SelectedData;
            }
            else if (this.state.filteredData) {
                dataScopeId = DataScopeId.FilteredData;
            }
            else {
                dataScopeId = DataScopeId.AllData;
            }
            return dataScopeId;
        }

        private activateDataBrowserItem(sideTabId: SideTabId, dataScopeId: DataScopeId) {
            if (!this.viewer) return;
            let itemToActivate: object;
            if (sideTabId === SideTabId.Data) {
                switch (dataScopeId) {
                    case DataScopeId.AllData: {
                        itemToActivate = this.state.dataContent && this.state.dataContent.data[this.state.selectedItemIndex[DataScopeId.AllData]];
                        break;
                    }
                    case DataScopeId.FilteredData: {
                        itemToActivate = this.state.filteredData && this.state.filteredData[this.state.selectedItemIndex[DataScopeId.FilteredData]];
                        break;
                    }
                    case DataScopeId.SelectedData: {
                        const selection = this.viewer.getSelection() || {};
                        itemToActivate = selection.selectedData && selection.selectedData[this.state.selectedItemIndex[DataScopeId.SelectedData]];
                        break;
                    }
                }
            }
            this.silentActivation(itemToActivate);
        }

        private silentActivation(itemToActivate: object) {
            this.ignoreSelectionChange = true;
            const done = () => {
                this.ignoreSelectionChange = false;
            };
            if (itemToActivate) {
                return this.viewer.activate(itemToActivate).then(done);
            } else {
                return this.viewer.deActivate().then(done);
            }
        }

        public sidebar(sidebarClosed: boolean, sidebarPinned: boolean) {
            this.setState({ sidebarClosed, sidebarPinned });
        }

        public resize() {
            this.setState({ calculating: () => this._resize() });
        }

        private _resize() {
            this.changeInsight(
                { size: this.getLayoutDivSize(this.state.sidebarPinned, this.state.sidebarClosed) },
                { label: 'resize', omit: true }
            );
        }

        private viewerMounted(glDiv: HTMLElement) {
            this.setState({
                size: this.getLayoutDivSize(this.state.sidebarPinned, this.state.sidebarClosed),
                signalValues: this.state.signalValues //keep initialized signalValues
            });
        }

        private getLayoutDivSize(pinned: boolean, closed: boolean) {
            const div = pinned && !closed ? this.layoutDivPinned : this.layoutDivUnpinned;
            return { height: div.offsetHeight, width: div.offsetWidth };
        }

        private toggleableSearch(e: TouchEvent | MouseEvent | PointerEvent, search: SandDance.searchExpression.SearchExpressionGroup) {
            if (e.ctrlKey) {
                this.setState({ search: createInputSearch(search) });
                this.setSideTabId(SideTabId.Search);
            } else {
                var oldSelection = this.viewer.getSelection();
                if (oldSelection.search) {
                    //look for matching groups and toggle them
                    const result = toggleSearch(SandDance.searchExpression.ensureSearchExpressionGroupArray(oldSelection.search), search);
                    if (result.found) {
                        //removing a group
                        if (result.groups.length === 0) {
                            this.doDeselect();
                        } else {
                            //select with new search removed
                            this.doSelect(result.groups);
                        }
                    } else {
                        //adding a new group
                        if (e.altKey || e.shiftKey) {
                            let group = true;
                            if (e.altKey) {
                                search.clause = '&&';
                            } else if (e.shiftKey) {
                                if (this.props.searchORDisabled) {
                                    group = false;
                                } else {
                                    search.clause = '||';
                                }
                            }
                            if (group) {
                                result.groups.push(search);
                                this.doSelect(result.groups);
                            } else {
                                this.doSelect(search);
                            }
                        } else {
                            //replace
                            this.doSelect(search);
                        }
                    }
                } else {
                    this.doSelect(search);
                }
            }
        }

        private doFilter(search: SandDance.searchExpression.Search, historicFilterChange: string) {
            this.historicFilterChange = historicFilterChange;
            this.viewer.filter(search);
        }

        private doUnfilter(historicFilterChange: string) {
            this.historicFilterChange = historicFilterChange;
            this.viewer.reset();
        }

        private doSelect(search: SandDance.searchExpression.Search) {
            this.viewer.select(search);
        }

        private doDeselect() {
            return this.viewer.deselect();
        }

        private writeSnapshot(snapshot: Snapshot, editIndex: number) {
            let { selectedSnapshotIndex } = this.state;
            let snapshots: Snapshot[];
            if (editIndex >= 0) {
                snapshots = [...this.state.snapshots];
                snapshots[editIndex] = snapshot;
                this.setState({ snapshots, selectedSnapshotIndex });
            } else {
                const note = snapshot.description;
                snapshots = this.state.snapshots.concat(snapshot);
                selectedSnapshotIndex = snapshots.length - 1;
                this.scrollSnapshotIntoView(selectedSnapshotIndex);
                this.setState({ sideTabId: SideTabId.Snapshots, snapshots, selectedSnapshotIndex, note });
            }
            this.props.onSnapshotsChanged && this.props.onSnapshotsChanged(snapshots);
        }

        public scrollSnapshotIntoView(selectedSnapshotIndex: number) {
            clearTimeout(this.scrollSnapshotTimer);
            if (this.state.sidebarClosed) return;
            this.scrollSnapshotTimer = setTimeout(() => {
                const selectedSnapshotElement = this.div.querySelector(`.snapshot:nth-child(${selectedSnapshotIndex + 1})`) as HTMLElement;
                if (selectedSnapshotElement) {
                    selectedSnapshotElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
                }
            }, 500) as any as number;
        }

        componentDidMount() {
            if (this.props.mounted) {
                this.props.mounted(this as any as Explorer_Class);
            }
        }

        render() {
            const { colorBin, columns, directColor, facetStyle, filter, hideAxes, hideLegend, scheme, signalValues, size, totalStyle, transform, chart, view } = this.state;
            const insight: SandDance.specs.Insight = {
                colorBin,
                columns,
                directColor,
                facetStyle,
                filter,
                hideAxes,
                hideLegend,
                scheme,
                signalValues,
                size,
                totalStyle,
                transform,
                chart,
                view
            };

            const loaded = !!(this.state.columns && this.state.dataContent);

            const selectionState: SandDance.types.SelectionState = (this.viewer && this.viewer.getSelection()) || {};
            const selectionSearch = selectionState && selectionState.search;

            const columnMapProps = this.getColumnMapBaseProps();

            const datas: { [key: number]: object[] } = {};
            datas[DataScopeId.AllData] = this.state.dataContent && this.state.dataContent.data;
            datas[DataScopeId.FilteredData] = this.state.filteredData;
            datas[DataScopeId.SelectedData] = selectionState && selectionState.selectedData;

            if (this.state.calculating) {
                requestAnimationFrame(() => {
                    //allow render to complete
                    if (this.state.calculating) {
                        this.state.calculating();
                        this.setState({ calculating: null });
                    }
                });
            }

            const theme = this.props.theme || '';
            const themePalette = themePalettes[theme];

            return (
                <div
                    ref={div => { if (div) this.div = div; }}
                    className={util.classList('sanddance-explorer', this.props.theme)}
                >
                    <Topbar
                        collapseLabels={this.props.compactUI}
                        historyIndex={this.state.historyIndex}
                        historyItems={this.state.historyItems}
                        undo={() => this.undo()}
                        redo={() => this.redo()}
                        logoClickUrl={this.props.logoClickUrl}
                        logoClickTarget={this.props.logoClickTarget}
                        themePalette={themePalette}
                        loaded={loaded}
                        doDeselect={this.doDeselect.bind(this)}
                        doFilter={this.doFilter.bind(this)}
                        doUnfilter={this.doUnfilter.bind(this)}
                        filter={this.state.filter}
                        selectionSearch={selectionSearch}
                        selectionState={selectionState}
                        buttons={this.props.topBarButtonProps}
                        view={this.state.view}
                        snapshots={this.state.snapshots}
                        onSnapshotPreviousClick={() => {
                            let selectedSnapshotIndex: number;
                            if (this.state.selectedSnapshotIndex === -1) {
                                selectedSnapshotIndex = this.state.snapshots.length - 1;
                            } else {
                                selectedSnapshotIndex = this.state.selectedSnapshotIndex;
                                selectedSnapshotIndex--;
                                if (selectedSnapshotIndex < 0) {
                                    selectedSnapshotIndex = this.state.snapshots.length - 1;
                                }
                            }
                            this.handleReviveSnapshot(this.state.snapshots[selectedSnapshotIndex], selectedSnapshotIndex);
                        }}
                        onSnapshotClick={() => this.snapshotEditor.editSnapshot()}
                        onSnapshotNextClick={() => {
                            let selectedSnapshotIndex: number;
                            if (this.state.selectedSnapshotIndex === -1) {
                                selectedSnapshotIndex = 0;
                            } else {
                                selectedSnapshotIndex = this.state.selectedSnapshotIndex;
                                selectedSnapshotIndex++;
                                if (selectedSnapshotIndex > this.state.snapshots.length - 1) {
                                    selectedSnapshotIndex = 0;
                                }
                            }
                            this.handleReviveSnapshot(this.state.snapshots[selectedSnapshotIndex], selectedSnapshotIndex);
                        }}
                        onViewClick={() => {
                            const view = this.state.view === '2d' ? '3d' : '2d';
                            this.changeInsight(
                                { view },
                                { label: view === '2d' ? strings.labelViewType2d : strings.labelViewType3d }
                            );
                        }}
                        onHomeClick={() => this.viewer.presenter.homeCamera()}
                    />
                    <div className={util.classList('sanddance-main', this.state.sidebarPinned && 'pinned', this.state.sidebarClosed && 'closed', (insight.hideLegend || insight.directColor || !colorMapping(insight, this.state.dataContent && this.state.dataContent.columns)) && 'hide-legend')}>
                        <div ref={div => { if (div && !this.layoutDivUnpinned) this.layoutDivUnpinned = div; }} className="sanddance-layout-unpinned"></div>
                        <div ref={div => { if (div && !this.layoutDivPinned) this.layoutDivPinned = div; }} className="sanddance-layout-pinned"></div>
                        {!loaded && (
                            <div className="loading">
                                <base.fluentUI.Spinner
                                    size={base.fluentUI.SpinnerSize.large}
                                    label={strings.loading}
                                />
                            </div>
                        )}
                        <Sidebar
                            themePalette={themePalette}
                            calculating={!!this.state.calculating}
                            closed={this.state.sidebarClosed}
                            hideSidebarControls={this.props.hideSidebarControls}
                            pinned={this.state.sidebarPinned}
                            disabled={!loaded}
                            dataScopeProps={{
                                themePalette,
                                compact: this.state.sidebarClosed,
                                onCompactClick: () => {
                                    this.changeInsight(
                                        {
                                            size: this.getLayoutDivSize(this.state.sidebarPinned, false)
                                        },
                                        {
                                            label: null, omit: true
                                        },
                                        {
                                            sidebarClosed: false,
                                        }
                                    );
                                },
                                dataSet: this.props.datasetElement,
                                dataCount: loaded && {
                                    all: this.state.dataContent && this.state.dataContent.data.length,
                                    filtered: this.state.filteredData && this.state.filteredData.length,
                                    selected: selectionState && selectionState.selectedData && selectionState.selectedData.length
                                },
                                active: this.state.sideTabId === SideTabId.Data,
                                onDataScopeClick: dataScopeId => this.setSideTabId(SideTabId.Data, dataScopeId),
                                selectedDataScope: this.state.dataScopeId,
                                disabled: !loaded
                            }}
                            onSideTabClick={sideTabId => {
                                //collapse or toggle
                                if (sideTabId === SideTabId.Collapse || this.state.sideTabId === sideTabId) {
                                    let { dataScopeId, sidebarClosed } = this.state;
                                    if (sidebarClosed && sideTabId === SideTabId.Data) {
                                        dataScopeId = this.getBestDataScopeId();
                                    }
                                    sidebarClosed = !this.state.sidebarClosed;
                                    this.changeInsight(
                                        {
                                            size: this.getLayoutDivSize(this.state.sidebarPinned, sidebarClosed)
                                        },
                                        {
                                            label: null, omit: true
                                        },
                                        {
                                            dataScopeId,
                                            sidebarClosed,
                                        }
                                    );
                                } else if (sideTabId === SideTabId.Pin) {
                                    this.changeInsight(
                                        {
                                            size: this.getLayoutDivSize(!this.state.sidebarPinned, this.state.sidebarClosed)
                                        },
                                        {
                                            label: null, omit: true
                                        },
                                        {
                                            sidebarPinned: !this.state.sidebarPinned
                                        }
                                    );
                                } else {
                                    this.setSideTabId(sideTabId);
                                }
                            }}
                            selectedSideTab={this.state.sideTabId}
                        >
                            {loaded && (() => {
                                switch (this.state.sideTabId) {
                                    case SideTabId.ChartType: {
                                        return (
                                            <Chart
                                                collapseLabels={this.props.compactUI}
                                                tooltipExclusions={this.state.tooltipExclusions}
                                                toggleTooltipExclusion={columnName => {
                                                    const tooltipExclusions = [...this.state.tooltipExclusions];
                                                    const i = tooltipExclusions.indexOf(columnName);
                                                    if (i < 0) {
                                                        tooltipExclusions.push(columnName);
                                                    } else {
                                                        tooltipExclusions.splice(i, 1);
                                                    }
                                                    this.setState({ tooltipExclusions });
                                                    this.props.onTooltipExclusionsChanged && this.props.onTooltipExclusionsChanged(tooltipExclusions);
                                                }}
                                                disabled={!loaded || this.state.sidebarClosed}
                                                {...columnMapProps}
                                                chart={this.state.chart}
                                                view={this.state.view}
                                                onChangeChartType={chart => this.changeChartType(chart)}
                                                insightColumns={this.state.columns}
                                                onChangeSignal={(role, column, name, value) => saveSignalValuePref(this.prefs, this.state.chart, role, column, name, value)}
                                            />
                                        );
                                    }
                                    case SideTabId.Color: {
                                        return (
                                            <Color
                                                compactUI={this.props.compactUI}
                                                specCapabilities={this.state.specCapabilities}
                                                disabled={!loaded || this.state.sidebarClosed}
                                                {...columnMapProps}
                                                dataContent={this.state.dataContent}
                                                scheme={this.state.scheme}
                                                colorBin={this.state.colorBin}
                                                colorBinSignal={this.viewer && this.viewer.vegaSpec && this.viewer.vegaSpec.signals.filter(s => s.name === SandDance.constants.SignalNames.ColorBinCount)[0]}
                                                colorReverseSignal={this.viewer && this.viewer.vegaSpec && this.viewer.vegaSpec.signals.filter(s => s.name === SandDance.constants.SignalNames.ColorReverse)[0]}
                                                colorColumn={this.state.columns.color}
                                                onColorBinChange={colorBin => {
                                                    this.ignoreSelectionChange = true;
                                                    this.viewer.deselect().then(() => {
                                                        this.ignoreSelectionChange = false;
                                                        //allow deselection to render
                                                        requestAnimationFrame(() => {
                                                            this.getColorContext = null;
                                                            this.changeInsight(
                                                                { colorBin },
                                                                { label: strings.labelHistoryColorBin }
                                                            );
                                                            savePref(this.prefs, this.state.chart, 'color', this.state.columns.color, { colorBin });
                                                        });
                                                    });
                                                }}
                                                onColorSchemeChange={(scheme) => {
                                                    this.changeColumnMapping('color', this.state.dataContent.columns.filter(c => c.name === this.state.columns.color)[0], { scheme });
                                                    savePref(this.prefs, this.state.chart, 'color', this.state.columns.color, { scheme });
                                                }}
                                                onColorBinCountChange={value => {
                                                    const signalValues: SandDance.specs.SignalValues = {};
                                                    signalValues[SandDance.constants.SignalNames.ColorBinCount] = value;
                                                    savePref(this.prefs, this.state.chart, 'color', this.state.columns.color, { signalValues });
                                                }}
                                                onColorReverseChange={value => {
                                                    this.getColorContext = null;
                                                }}
                                                directColor={this.state.directColor}
                                                onDirectColorChange={directColor => {
                                                    this.changeInsight(
                                                        { directColor },
                                                        { label: strings.labelHistoryDirectColor },
                                                        { calculating: () => this._resize() }
                                                    );
                                                }}
                                            />
                                        );
                                    }
                                    case SideTabId.Data: {
                                        const data = datas[this.state.dataScopeId];
                                        let itemVisible = true;
                                        switch (this.state.dataScopeId) {
                                            case DataScopeId.AllData: {
                                                const item = this.state.selectedItemIndex[this.state.dataScopeId];
                                                itemVisible = this.state.dataContent && !this.state.filteredData || this.state.filteredData.indexOf(data[item]) >= 0;
                                            }
                                        }
                                        return (
                                            <DataBrowser
                                                theme={this.props.theme}
                                                themePalette={themePalette}
                                                disabled={!loaded || this.state.sidebarClosed}
                                                columns={this.state.dataContent && this.state.dataContent.columns}
                                                data={data}
                                                displayName={(this.state.dataFile && this.state.dataFile.displayName) || strings.defaultFileName}
                                                nullMessage={dataBrowserNullMessages[this.state.dataScopeId]}
                                                zeroMessage={dataBrowserZeroMessages[this.state.dataScopeId]}
                                                index={this.state.selectedItemIndex[this.state.dataScopeId]}
                                                itemVisible={itemVisible}
                                                dataExportHandler={this.props.dataExportHandler}
                                                selectedDataScope={this.state.dataScopeId}
                                                onDataScopeClick={dataScopeId => this.setSideTabId(SideTabId.Data, dataScopeId)}
                                                onActivate={(row, index) => {
                                                    const selectedItemIndex = { ...this.state.selectedItemIndex };
                                                    selectedItemIndex[this.state.dataScopeId] = index;
                                                    this.setState({ selectedItemIndex });
                                                    this.silentActivation(row);
                                                }}
                                                onSearch={(e, search) => {
                                                    if (e.ctrlKey) {
                                                        this.setState({ sideTabId: SideTabId.Search, search });
                                                    } else {
                                                        this.doSelect(search);
                                                    }
                                                }}
                                                bingSearchDisabled={this.props.bingSearchDisabled}
                                            />
                                        );
                                    }
                                    case SideTabId.Search: {
                                        return (
                                            <Search
                                                collapseLabels={this.props.compactUI}
                                                themePalette={themePalette}
                                                disabled={!loaded || this.state.sidebarClosed}
                                                disableGroupOR={this.props.searchORDisabled}
                                                disableExpressionOR={this.props.searchORDisabled}
                                                initializer={{
                                                    columns: columnMapProps.allColumns,
                                                    search: this.state.search
                                                }}
                                                autoCompleteDistinctValues={this.state.autoCompleteDistinctValues}
                                                onSelect={expr => this.doSelect(expr)}
                                                data={this.state.dataContent.data}
                                            />
                                        );
                                    }
                                    case SideTabId.Snapshots: {
                                        return (
                                            <Snapshots
                                                {...this.props.snapshotProps}
                                                editor={this.snapshotEditor}
                                                themePalette={themePalette}
                                                explorer={this as any as Explorer_Class}
                                                snapshots={this.state.snapshots}
                                                selectedSnapshotIndex={this.state.selectedSnapshotIndex}
                                                onClearSnapshots={() => {
                                                    const snapshots = [];
                                                    this.setState({ snapshots, selectedSnapshotIndex: -1 });
                                                    this.props.onSnapshotsChanged && this.props.onSnapshotsChanged(snapshots);
                                                }}
                                                onWriteSnapshot={(s, i) => this.writeSnapshot(s, i)}
                                                onRemoveSnapshot={i => {
                                                    const snapshots = [...this.state.snapshots];
                                                    snapshots.splice(i, 1);
                                                    let { selectedSnapshotIndex } = this.state;
                                                    if (i === selectedSnapshotIndex) {
                                                        selectedSnapshotIndex = -1;
                                                    } else if (selectedSnapshotIndex > i) {
                                                        selectedSnapshotIndex--;
                                                    }
                                                    this.setState({ snapshots, selectedSnapshotIndex });
                                                    this.props.onSnapshotsChanged && this.props.onSnapshotsChanged(snapshots);
                                                }}
                                                onSnapshotClick={(snapshot, selectedSnapshotIndex) => {
                                                    this.setState({ selectedSnapshotIndex });
                                                    this.calculate(() => {
                                                        this.handleReviveSnapshot(snapshot, selectedSnapshotIndex);
                                                    });
                                                }}
                                                onMoveUp={i => {
                                                    if (i > 0) {
                                                        const snapshots = [...this.state.snapshots];
                                                        const temp = snapshots[i - 1];
                                                        snapshots[i - 1] = snapshots[i];
                                                        snapshots[i] = temp;
                                                        let { selectedSnapshotIndex } = this.state;
                                                        if (i === selectedSnapshotIndex) {
                                                            selectedSnapshotIndex = i - 1;
                                                        } else if (i - 1 === selectedSnapshotIndex) {
                                                            selectedSnapshotIndex = i;
                                                        }
                                                        this.setState({ snapshots, selectedSnapshotIndex });
                                                        this.props.onSnapshotsChanged && this.props.onSnapshotsChanged(snapshots);
                                                    }
                                                }}
                                                onMoveDown={i => {
                                                    if (i < this.state.snapshots.length - 1) {
                                                        const snapshots = [...this.state.snapshots];
                                                        const temp = snapshots[i + 1];
                                                        snapshots[i + 1] = snapshots[i];
                                                        snapshots[i] = temp;
                                                        let { selectedSnapshotIndex } = this.state;
                                                        if (i === selectedSnapshotIndex) {
                                                            selectedSnapshotIndex = i + 1;
                                                        } else if (i + 1 === selectedSnapshotIndex) {
                                                            selectedSnapshotIndex = i;
                                                        }
                                                        this.setState({ snapshots, selectedSnapshotIndex });
                                                        this.props.onSnapshotsChanged && this.props.onSnapshotsChanged(snapshots);
                                                    }
                                                }}
                                            />
                                        );
                                    }
                                    case SideTabId.History: {
                                        return (
                                            <History
                                                theme={theme}
                                                themePalette={themePalette}
                                                historyIndex={this.state.historyIndex}
                                                historyItems={this.state.historyItems}
                                                redo={i => this.redo(i)}
                                            />
                                        );
                                    }
                                    case SideTabId.Settings: {
                                        return (
                                            <Settings
                                                explorer={this as any as Explorer_Class}
                                                dataFile={this.state.dataFile}
                                                scheme={this.state.scheme}
                                                hideLegend={this.state.hideLegend}
                                                onToggleLegend={hideLegend => this.setState({ hideLegend, calculating: () => this._resize() })}
                                                hideAxes={this.state.hideAxes}
                                                onToggleAxes={hideAxes => this.setState({ calculating: () => this.setState({ hideAxes }) })}
                                                additionalSettings={this.props.additionalSettings}
                                            >
                                                {this.props.systemInfoChildren}
                                            </Settings>
                                        );
                                    }
                                }
                            })()}
                        </Sidebar>
                        {loaded && (
                            <div className="sanddance-view">
                                <SandDanceReact
                                    renderOptions={{
                                        rebaseFilter: () => {
                                            const { rebaseFilter } = this;
                                            if (rebaseFilter) {
                                                this.rebaseFilter = false;
                                            }
                                            return rebaseFilter;
                                        },
                                        initialColorContext: this.getColorContext && this.getColorContext(this.viewer.insight, insight),
                                        discardColorContextUpdates: () => this.discardColorContextUpdates
                                    }}
                                    viewerOptions={this.viewerOptions}
                                    ref={reactViewer => {
                                        if (reactViewer) {
                                            this.viewer = reactViewer.viewer;
                                        }
                                    }}
                                    onView={renderResult => {
                                        this.changespecCapabilities(renderResult.specResult.errors ? renderResult.specResult.specCapabilities : this.viewer.specCapabilities);
                                        this.getColorContext = (oldInsight: SandDance.specs.Insight, newInsight: SandDance.specs.Insight) => {
                                            if (!oldInsight && !newInsight) {
                                                return null;
                                            }
                                            if (!oldInsight || !newInsight) {
                                                return null;
                                            }
                                            if (oldInsight.scheme !== newInsight.scheme) {
                                                return null;
                                            }
                                            if (oldInsight.columns.color !== newInsight.columns.color) {
                                                return null;
                                            }
                                            if (oldInsight.directColor != newInsight.directColor) {
                                                return null;
                                            }
                                            return this.viewer.colorContexts && this.viewer.colorContexts[this.viewer.currentColorContext];
                                        };
                                        //don't allow tabbing to the canvas
                                        removeTabIndex(this.viewer);
                                        this.props.onView && this.props.onView();
                                    }}
                                    onError={e => {
                                        this.props.onError && this.props.onError(e);
                                    }}
                                    data={this.state.dataContent.data}
                                    insight={insight}
                                    onMount={el => this.viewerMounted(el)}
                                />
                                {this.state.note && (
                                    <div className='sanddance-note'>
                                        <IconButton
                                            className='cancel'
                                            themePalette={themePalette}
                                            title={strings.buttonClose}
                                            iconName='Cancel'
                                            onClick={() => this.setState({ note: null })}
                                        />
                                        <div>{this.state.note}</div>
                                    </div>
                                )}
                            </div>
                        )}
                        <Dialog
                            title={strings.labelError}
                            hidden={!this.state.errors}
                            onDismiss={() => {
                                this.setState({ errors: null });
                            }}
                        >
                            {this.state.errors && this.state.errors.map((error, i) => (
                                <div key={i}>{error}</div>
                            ))}
                        </Dialog>
                        <SnapshotEditor
                            ref={se => this.snapshotEditor = se}
                            {...this.props.snapshotProps}
                            explorer={this as any as Explorer_Class}
                            onWriteSnapshot={(s, i) => this.writeSnapshot(s, i)}
                            theme={this.props.theme}
                            themePalette={themePalette}
                        />
                    </div>
                    {this.state.positionedColumnMapProps && (
                        <PositionedColumnMap
                            {...this.state.positionedColumnMapProps}
                        />
                    )}
                </div>
            );
        }