diff --git a/README.md b/README.md index 2bfcbdbb9..8bc765ed4 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ const libVoyager = require('voyager'); const container = document.getElementById("voyager-embed"); const config = undefined; const data = undefined; -const voyagerInstance = libVoyager.CreateVoyager(container, config, data) +const voyagerInstance = libVoyager.createVoyager(container, {config, data}) ``` Initializing with data @@ -71,14 +71,14 @@ const data: any = { ] }; -const voyagerInstance = libVoyager.CreateVoyager(container, undefined, data) +const voyagerInstance = libVoyager.createVoyager(container, {config: undefined, data}) ``` Updating Data ```js -const voyagerInstance = libVoyager.CreateVoyager(container, undefined, undefined) +const voyagerInstance = libVoyager.createVoyager(container) const data: any = { "values": [ @@ -103,16 +103,16 @@ You currently also need to include the CSS. Note that this has not yet been opti The voyager _module_ exposes 1 function. -#### CreateVoyager(container, config, data) +#### createVoyager(container, VoyagerParams) ```js /** - * Create an instance of the voyager application and return it. + * Create an instance of the voyager application. * - * @param {Container} container css selector or HTMLElement that will be the parent - * element of the application - * @param {Object|undefined} config Optional: configuration options - * @param {Array|undefined} data Optional: data object. Can be a string or an array of objects. + * @param {Container} container css selector or HTMLElement that will be the parent + * element of the application + * @param {VoyagerParams} params Voyager params. {data, config}. Any new additional parameter + * required by voyager can be added here as named parameter for easier visibilty */ ``` diff --git a/src/components/bookmark/index.tsx b/src/components/bookmark/index.tsx index b467e7cb3..324692376 100644 --- a/src/components/bookmark/index.tsx +++ b/src/components/bookmark/index.tsx @@ -7,9 +7,10 @@ import {BOOKMARK_CLEAR_ALL, BookmarkAction} from '../../actions/bookmark'; import {ActionHandler, createDispatchHandler} from '../../actions/redux-action'; import {State} from '../../models'; import {Bookmark} from '../../models/bookmark'; +import { VoyagerConfig } from '../../models/config'; import {ResultPlot} from '../../models/result'; import {selectData} from '../../selectors/dataset'; -import {selectBookmark} from '../../selectors/index'; +import {selectBookmark, selectConfig} from '../../selectors/index'; import {Plot} from '../plot'; import * as styles from './bookmark.scss'; @@ -17,6 +18,7 @@ import * as styles from './bookmark.scss'; export interface BookmarkProps extends ActionHandler { bookmark: Bookmark; data: InlineData; + config: VoyagerConfig; } export class BookmarkBase extends React.PureComponent { @@ -99,7 +101,7 @@ export class BookmarkBase extends React.PureComponent { } private renderBookmarks(bookmark: Bookmark) { - const {data} = this.props; + const {data, config} = this.props; const plots: ResultPlot[] = bookmark.list.map(key => bookmark.dict[key].plot); const bookmarkPlotListItems = plots.map((plot, index) => { @@ -116,6 +118,7 @@ export class BookmarkBase extends React.PureComponent { isPlotListItem={true} showBookmarkButton={true} showSpecifyButton={true} + config={config} spec={spec} /> ); @@ -139,7 +142,8 @@ export const BookmarkPane = connect( (state: State) => { return { bookmark: selectBookmark(state), - data: selectData(state) + data: selectData(state), + config: selectConfig(state) }; }, createDispatchHandler() diff --git a/src/components/plot-list/index.tsx b/src/components/plot-list/index.tsx index fbbd47c2b..52483d109 100644 --- a/src/components/plot-list/index.tsx +++ b/src/components/plot-list/index.tsx @@ -12,11 +12,12 @@ import { import {ShelfAction} from '../../actions/shelf'; import {SPINNER_COLOR} from '../../constants'; import {Bookmark} from '../../models/bookmark'; +import { VoyagerConfig } from '../../models/config'; import {State} from '../../models/index'; import {ResultType} from '../../models/result'; import {Result} from '../../models/result/index'; import {ShelfFilter} from '../../models/shelf/filter'; -import {selectFilteredData} from '../../selectors/index'; +import {selectConfig, selectFilteredData} from '../../selectors/index'; import {selectFilters} from '../../selectors/shelf'; import {Plot} from '../plot'; import * as styles from './plot-list.scss'; @@ -33,6 +34,8 @@ export interface PlotListConnectProps { data: InlineData; filters: ShelfFilter[]; + + config: VoyagerConfig; } export type PlotListProps = PlotListOwnProps & PlotListConnectProps; @@ -45,7 +48,7 @@ export class PlotListBase extends React.PureComponent { } public render() { - const {handleAction, bookmark, data, filters, result} = this.props; + const {handleAction, bookmark, data, filters, result, config} = this.props; const {plots, limit, isLoading} = result; const plotListItems = plots && plots.slice(0, limit).map((plot, index) => { const {spec, fieldInfos} = plot; @@ -62,6 +65,7 @@ export class PlotListBase extends React.PureComponent { showSpecifyButton={true} spec={spec} bookmark={bookmark} + config={config} /> ); }); @@ -116,7 +120,8 @@ export const PlotList = connect( // take spec from props and read spec.data.name return { data: selectFilteredData(state), - filters: selectFilters(state) + filters: selectFilters(state), + config: selectConfig(state) }; } )(CSSModules(PlotListBase, styles)); diff --git a/src/components/plot/index.tsx b/src/components/plot/index.tsx index 0923f9ab1..ca7a00645 100644 --- a/src/components/plot/index.tsx +++ b/src/components/plot/index.tsx @@ -14,6 +14,7 @@ import {ShelfAction, SPEC_LOAD} from '../../actions/shelf'; import {SHELF_PREVIEW_DISABLE, SHELF_PREVIEW_SPEC, ShelfPreviewAction} from '../../actions/shelf-preview'; import {PLOT_HOVER_MIN_DURATION} from '../../constants'; import {Bookmark} from '../../models/bookmark'; +import { VoyagerConfig } from '../../models/config'; import {PlotFieldInfo, ResultPlot} from '../../models/result'; import {ShelfFilter, toTransforms} from '../../models/shelf/filter'; import {Field} from '../field/index'; @@ -40,9 +41,9 @@ export interface PlotProps extends ActionHandler< // specified when it's in the modal // so we can close the modal when the specify button is clicked. closeModal?: () => void; + config: VoyagerConfig; } - export interface PlotState { hovered: boolean; preview: boolean; @@ -89,7 +90,7 @@ export class PlotBase extends React.PureComponent { } public render() { - const {isPlotListItem, onSort, showBookmarkButton, showSpecifyButton, spec, data} = this.props; + const {isPlotListItem, onSort, showBookmarkButton, showSpecifyButton, spec, data, config} = this.props; let notesDiv; const specKey = JSON.stringify(spec); @@ -137,7 +138,7 @@ export class PlotBase extends React.PureComponent { onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} > - + {notesDiv} @@ -341,4 +342,5 @@ export class PlotBase extends React.PureComponent { } } + export const Plot = CSSModules(PlotBase, styles); diff --git a/src/components/vega-lite/index.tsx b/src/components/vega-lite/index.tsx index fec4f105a..0175580d5 100644 --- a/src/components/vega-lite/index.tsx +++ b/src/components/vega-lite/index.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import {ClipLoader} from 'react-spinners'; import * as vega from 'vega'; import * as vl from 'vega-lite'; +import {Config} from 'vega-lite/build/src/config'; import {InlineData, isNamedData} from 'vega-lite/build/src/data'; import {TopLevelExtendedSpec} from 'vega-lite/build/src/spec'; import * as vegaTooltip from 'vega-tooltip'; @@ -16,6 +17,8 @@ export interface VegaLiteProps { logger: Logger; data: InlineData; + + config?: Config; } export interface VegaLiteState { @@ -72,8 +75,15 @@ export class VegaLite extends React.PureComponent // "y": {"field": "b", "type": "quantitative"} // } // }; - const {logger} = this.props; - const vlSpec = this.props.spec; + const {logger, config} = this.props; + let vlSpec = this.props.spec; + vlSpec = { + ...vlSpec, + config: { + ...vlSpec.config, + ...config + } + }; try { const spec = vl.compile(vlSpec, logger).spec; const runtime = vega.parse(spec, vlSpec.config); diff --git a/src/components/view-pane/index.tsx b/src/components/view-pane/index.tsx index e7b04d46c..2a9f89938 100644 --- a/src/components/view-pane/index.tsx +++ b/src/components/view-pane/index.tsx @@ -102,7 +102,7 @@ class ViewPaneBase extends React.PureComponent { } private renderSpecifiedView() { - const {bookmark, data, filters, handleAction, spec} = this.props; + const {bookmark, data, filters, handleAction, spec, config} = this.props; if (spec) { return ( @@ -114,6 +114,7 @@ class ViewPaneBase extends React.PureComponent { onSort={this.onSort} showBookmarkButton={true} spec={spec} + config={config} /> ); } else { diff --git a/src/index.tsx b/src/index.tsx index 09572843a..a20432fc6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -18,9 +18,9 @@ const data: Data = undefined; ReactDOM.render( , document.getElementById('root') diff --git a/src/lib-voyager.test.ui.tsx b/src/lib-voyager.test.ui.tsx index 42719eff1..5f3aa2ee9 100644 --- a/src/lib-voyager.test.ui.tsx +++ b/src/lib-voyager.test.ui.tsx @@ -3,11 +3,15 @@ */ import * as ReactDOM from 'react-dom'; -import {CreateVoyager} from './lib-voyager'; +import {Config} from 'vega-lite/build/src/config'; +import {Data} from 'vega-lite/build/src/data'; +import {createVoyager} from './lib-voyager'; +import {VoyagerConfig} from './models/config'; import {SerializableState} from './models/index'; const DEFAULT_TIMEOUT_LENGTH = 300; + describe('lib-voyager', () => { let container: HTMLElement; @@ -35,7 +39,7 @@ describe('lib-voyager', () => { setTimeout(() => { try { - const voyagerInst = CreateVoyager(container, undefined, undefined); + const voyagerInst = createVoyager(container); const dataPaneHeader = document.querySelector('.load-data-pane__load-data-pane'); expect(dataPaneHeader.textContent).toContain('Please load a dataset'); @@ -69,12 +73,142 @@ describe('lib-voyager', () => { }); }); + describe('CreateVoyager, pass vegalite configuration', () => { + it('initializes with empty config', done => { + const data: Data = undefined; + const config: Config = undefined; + const voyagerParams = {data, config}; + setTimeout(() => { + try { + createVoyager(container, voyagerParams); + const dataPaneHeader = document.querySelector('.load-data-pane__load-data-pane'); + expect(dataPaneHeader.textContent).toContain('Please load a dataset'); + + setTimeout(() => { + try { + const fieldList = document.querySelectorAll('.field-list__field-list-item'); + expect(fieldList.length).toEqual(0); + done(); + } catch (err) { + done.fail(err); + } + }, DEFAULT_TIMEOUT_LENGTH); + } catch (err) { + done.fail(err); + } + }, 10); + }); + it ('accepts data and no config', done => { + const data: any = { + "values": [ + {"fieldA": "A", "fieldB": 28}, {"fieldA": "B", "fieldB": 55}, {"fieldA": "C", "fieldB": 43}, + {"fieldA": "D", "fieldB": 91}, {"fieldA": "E", "fieldB": 81}, {"fieldA": "F", "fieldB": 53}, + {"fieldA": "G", "fieldB": 19}, {"fieldA": "H", "fieldB": 87}, {"fieldA": "I", "fieldB": 52} + ] + }; + const config: Config = undefined; + const voyagerParams = {data, config}; + setTimeout(() => { + try { + createVoyager(container, voyagerParams); + setTimeout(() => { + try { + const fieldList = document.querySelectorAll('.field-list__field-list-item'); + expect(fieldList.length).toEqual(5); + done(); + } catch (err) { + done.fail(err); + } + }, DEFAULT_TIMEOUT_LENGTH); + } catch (err) { + done.fail(err); + } + }, 10); + }); + + it ('accepts data and vegalite config', done => { + const data: any = { + "values": [ + {"fieldA": "A", "fieldB": 28}, {"fieldA": "B", "fieldB": 55}, {"fieldA": "C", "fieldB": 43}, + {"fieldA": "D", "fieldB": 91}, {"fieldA": "E", "fieldB": 81}, {"fieldA": "F", "fieldB": 53}, + {"fieldA": "G", "fieldB": 19}, {"fieldA": "H", "fieldB": 87}, {"fieldA": "I", "fieldB": 52} + ] + }; + const config = { + vegaliteConfig: { + mark: { + color: 'black' + } + } + }; + const voyagerParams = {data, config}; + setTimeout(() => { + try { + const voyagerInstance = createVoyager(container, voyagerParams); + setTimeout(() => { + try { + const state = voyagerInstance.getApplicationState(); + expect(state.config.vegaliteConfig.mark.color).toBe('black'); + done(); + } catch (err) { + done.fail(err); + } + }, DEFAULT_TIMEOUT_LENGTH); + } catch (err) { + done.fail(err); + } + }, 10); + }); + it('creates a Voyager instance, update config and update data should retain correct config', done => { + let data; + const config = { + vegaliteConfig: { + mark: { + color: 'black' + } + } + }; + const voyagerParams = {data, config}; + setTimeout(() => { + try { + const voyagerInstance = createVoyager(container, voyagerParams); + let state = voyagerInstance.getApplicationState(); + expect(state.config.vegaliteConfig.mark.color).toBe('black'); + data = { + "values": [ + {"fieldA": "A", "fieldB": 28}, {"fieldA": "B", "fieldB": 55}, {"fieldA": "C", "fieldB": 43}, + {"fieldA": "D", "fieldB": 91}, {"fieldA": "E", "fieldB": 81}, {"fieldA": "F", "fieldB": 53}, + {"fieldA": "G", "fieldB": 19}, {"fieldA": "H", "fieldB": 87}, {"fieldA": "I", "fieldB": 52} + ] + }; + voyagerInstance.updateData(data); + state = voyagerInstance.getApplicationState(); + expect(state.config.vegaliteConfig.mark.color).toBe('black'); + voyagerInstance.updateConfig( + { + vegaliteConfig: { + mark: { + color: 'blue' + } + } + } + ); + state = voyagerInstance.getApplicationState(); + expect(state.config.vegaliteConfig.mark.color).toBe('blue'); + done(); + } catch (err) { + done.fail(err); + } + }, 10); + }); + }); + describe('get/setApplicationState', () => { it('gets and sets application state', done => { setTimeout(() => { try { - const voyagerInst = CreateVoyager(container, undefined, undefined); + const voyagerInst = createVoyager(container); const state = voyagerInst.getApplicationState(); expect(state).toHaveProperty('config'); @@ -114,7 +248,7 @@ describe('lib-voyager', () => { setTimeout(() => { try { - const voyagerInst = CreateVoyager(container, undefined, undefined); + const voyagerInst = createVoyager(container); const aState = voyagerInst.getApplicationState(); const originalConfigOption = aState.config.showDataSourceSelector; @@ -150,7 +284,7 @@ describe('lib-voyager', () => { it('accepts valid spec', done => { setTimeout(() => { try { - const voyagerInst = CreateVoyager(container, undefined, undefined); + const voyagerInst = createVoyager(container); const spec: Object = { "$schema": "https://vega.github.io/schema/vega-lite/v2.json", @@ -199,8 +333,8 @@ describe('lib-voyager', () => { }); it('error on invalid spec', done => { - const config = {}; - const data: any = undefined; + const config: VoyagerConfig = {}; + const data: Data = undefined; const spec: Object = { "FAIL$schema": "https://vega.github.io/schema/vega-lite/v2.json", @@ -209,7 +343,10 @@ describe('lib-voyager', () => { "encoding": { } }; - const voyagerInst = CreateVoyager(container, config, data); + const voyagerInst = createVoyager(container, { + config, + data + }); // This should throw an exception; setTimeout(() => { diff --git a/src/lib-voyager.tsx b/src/lib-voyager.tsx index 09971023e..51e1e07cb 100644 --- a/src/lib-voyager.tsx +++ b/src/lib-voyager.tsx @@ -24,6 +24,10 @@ import {configureStore} from './store'; export type Container = string | HTMLElement; +export interface VoyagerParams { + data?: Data; + config?: VoyagerConfig; +}; /** * The Voyager class encapsulates the voyager application and allows for easy @@ -36,7 +40,8 @@ export class Voyager { private data: Data; private filename: string; - constructor(container: Container, config: VoyagerConfig, data: Data) { + constructor(container: Container, params: VoyagerParams) { + const {config = DEFAULT_VOYAGER_CONFIG, data} = params; if (isString(container)) { this.container = document.querySelector(container) as HTMLElement; // TODO throw error if not found @@ -48,6 +53,7 @@ export class Voyager { ...DEFAULT_VOYAGER_CONFIG, ...config }; + this.data = data; this.init(); } @@ -220,11 +226,10 @@ export class Voyager { /** * Create an instance of the voyager application. * - * @param {Container} container css selector or HTMLElement that will be the parent - * element of the application - * @param {Object} config configuration options - * @param {Array} data data object. Can be a string or an array of objects. + * @param {Container} container css selector or HTMLElement that will be the parent + * element of the application + * @param {VoyagerParams} params Voyager params. {data, config}. */ -export function CreateVoyager(container: Container, config: VoyagerConfig, data: Data): Voyager { - return new Voyager(container, config, data); +export function createVoyager(container: Container, params: VoyagerParams = {}): Voyager { + return new Voyager(container, params); } diff --git a/src/models/config.ts b/src/models/config.ts index eabadb0b4..51cc3abcd 100644 --- a/src/models/config.ts +++ b/src/models/config.ts @@ -1,9 +1,12 @@ +import {Config} from 'vega-lite/build/src/config'; + export interface VoyagerConfig { showDataSourceSelector?: boolean; serverUrl?: string | null; manualSpecificationOnly?: boolean; hideHeader?: boolean; hideFooter?: boolean; + vegaliteConfig?: Config; }; export const DEFAULT_VOYAGER_CONFIG: VoyagerConfig = { @@ -11,5 +14,6 @@ export const DEFAULT_VOYAGER_CONFIG: VoyagerConfig = { serverUrl: null, manualSpecificationOnly: false, hideHeader: false, - hideFooter: false + hideFooter: false, + vegaliteConfig: {} }; diff --git a/src/reducers/reset.test.ts b/src/reducers/reset.test.ts new file mode 100644 index 000000000..a5168bc2b --- /dev/null +++ b/src/reducers/reset.test.ts @@ -0,0 +1,84 @@ +import {combineReducers} from 'redux'; +import {RESET} from '../actions'; +import {makeResetReducer, ResetIndex} from './reset'; + +describe(RESET, () => { + + interface DummyState { + persistentState: object; + nonPersistentState: object; + } + + const DEFAULT_DUMMY_STATE = { + persistentState: {}, + nonPersistentState: {} + }; + + const persistentStateToReset: ResetIndex = { + persistentState: false, + nonPersistentState: true + }; + + const getResetReducer = (defaultState = DEFAULT_DUMMY_STATE) => { + return makeResetReducer( + combineReducers({ + persistentState: (state: any = defaultState.persistentState) => state, + nonPersistentState: (state: any = defaultState.nonPersistentState) => state + }), + persistentStateToReset, + defaultState + ); + }; + + it('should reset to right default value', () => { + const resetReducer = getResetReducer(); + expect( + resetReducer(DEFAULT_DUMMY_STATE, {type: RESET}) + ).toEqual(DEFAULT_DUMMY_STATE); + }); + + it('should reset bookmark when resetIndex is true', () => { + const resetReducer = getResetReducer(); + expect( + resetReducer(DEFAULT_DUMMY_STATE, {type: RESET}) + ).toEqual(DEFAULT_DUMMY_STATE); + const newModifiedState = { + ...DEFAULT_DUMMY_STATE, + nonPersistentState: { + ...DEFAULT_DUMMY_STATE.persistentState, + nonPersistentProperty1: 1, + nonPersistentProperty2: 2 + } + }; + expect( + resetReducer(newModifiedState, {type: 'NEWACTION'}) + ).toEqual(newModifiedState); + + expect( + resetReducer(newModifiedState, {type: RESET}) + ).toEqual(DEFAULT_DUMMY_STATE); + }); + + it('should not reset config when resetIndex is false', () => { + const modifiedDefaultState = { + ...DEFAULT_DUMMY_STATE, + persistentState: { + persistentStateProperty1: 1 + } + }; + const resetReducer = getResetReducer(modifiedDefaultState); + expect( + resetReducer(modifiedDefaultState, {type: RESET}) + ).toEqual(modifiedDefaultState); + const newModifiedState = { + ...modifiedDefaultState, + persistentState: { + persistentStateProperty2: 2, + persistentStateProperty3: 3 + } + }; + expect( + resetReducer(newModifiedState, {type: RESET}) + ).toEqual(newModifiedState); + }); +}); diff --git a/src/reducers/reset.ts b/src/reducers/reset.ts index 4f9618db2..4b5558de1 100644 --- a/src/reducers/reset.ts +++ b/src/reducers/reset.ts @@ -13,6 +13,10 @@ export function makeResetReducer( // Need to cast as object as TS somehow doesn't know that T extends object already const newState = {...state as object} as T; Object.keys(resetIndex).forEach((key: keyof T) => { + // This is to not reset any property if resetIndex[property] is false. + if (!resetIndex[key]) { + return; + } newState[key] = defaultValue[key]; }); return newState;