Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data faceting #538

Open
wants to merge 13 commits into
base: kb-MENG
Choose a base branch
from
14 changes: 14 additions & 0 deletions src/js/actions/facetLayoutActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {createStandardAction} from 'typesafe-actions';
import {FacetLayoutRecord} from '../store/factory/FacetLayout';
import {assignId} from '../util/counter';
import {State} from '../store';
import {Dispatch} from 'redux';

export function addFacetLayout (payload: FacetLayoutRecord) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: this file, facetLayoutReducer, and facetLayout.ts, i think they're related enough to core layout functionality that i would consider just adding the facet actions / reducer cases / record definitions to the existing layout files so that it's easier to find everything.

return function(dispatch: Dispatch, getState: () => State) {
const id = payload._id || assignId(dispatch, getState());
dispatch(baseAddFacetLayout(payload.merge({_id: id}), id));
};
}

export const baseAddFacetLayout = createStandardAction('ADD_FACET_LAYOUT')<FacetLayoutRecord, number>();
15 changes: 14 additions & 1 deletion src/js/actions/markActions.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import {batchGroupBy} from '../reducers/historyOptions';
import {State} from '../store';
import {LyraMarkType, Mark, MarkRecord, HandleStreams} from '../store/factory/Mark';
import {GroupRecord} from '../store/factory/marks/Group';
import {Facet} from 'vega-typings';
import {addGrouptoLayout} from './layoutActions';
import {assignId} from '../util/counter';
import {ThunkDispatch} from 'redux-thunk';
@@ -53,7 +54,19 @@ export function addGroup(record: GroupRecord, layoutId: number, dir: string) {
}
export const baseAddMark = createStandardAction('ADD_MARK')<{name: string, streams: HandleStreams, props: MarkRecord}, number>();


export function addFacet(facet: Facet, groupId: number) {
return function(dispatch: ThunkDispatch<State, any, any>, getState: () => State) {
batchGroupBy.start();
dispatch(baseAddGroupFacet(facet, groupId));
const childrenMarks = getState().getIn(['vis', 'present', 'marks', String(groupId), 'marks']);
childrenMarks.forEach(mark => {
dispatch(baseAddFacet(facet,mark));
});
batchGroupBy.end();
};
}
export const baseAddFacet = createStandardAction('ADD_FACET')<Facet, number>(); // number of mark ID
export const baseAddGroupFacet = createStandardAction('ADD_GROUP_FACET')<Facet, number>(); // number of Group ID
export const updateMarkProperty = createStandardAction('UPDATE_MARK_PROPERTY')<{property: string, value: any}, number>();

export const setParent = createStandardAction('SET_PARENT_MARK')<number, number>(); // parentId, childId
2 changes: 2 additions & 0 deletions src/js/components/index.tsx
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import {Toolbar} from './Toolbar';
import WidgetDropzone from './interactions/WidgetDropzone';
import MarkDropzoneGroup from './toolbar/MarkDropzoneGroup';
import MarkDropPlaceGroup from './toolbar/MarkDropPlaceGroup';
import FacetOptionsHolder from './pipelines/FacetOptionsHolder';

// React requires you only have one wrapper element called in your provider
module.exports = ReactDOM.render(
@@ -28,6 +29,7 @@ module.exports = ReactDOM.render(
<WidgetDropzone />
<MarkDropzoneGroup />
<MarkDropPlaceGroup />
<FacetOptionsHolder />
</div>
<Toolbar />
</div>
76 changes: 76 additions & 0 deletions src/js/components/pipelines/FacetDropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as React from 'react';
import { connect } from 'react-redux';
import {State} from '../../store';
import {FieldDraggingStateRecord} from '../../store/factory/Inspector';
// import {GroupFacet} from "../../store/factory/marks/Group";
import {Facet} from 'vega-typings';
import {getClosestGroupId} from '../../util/hierarchy';
import {addFacetLayout} from '../../actions/facetLayoutActions';
import {addFacet} from '../../actions/markActions';
import {FacetLayout} from '../../store/factory/FacetLayout';
interface StateProps {
dragging: FieldDraggingStateRecord;
groupId: number;
}

interface OwnProps {
layoutOrientation: string
}
interface DispatchProps {
facetField: (field: string, groupId: number) => void;
}

function mapStateToProps(state: State): StateProps {
const groupId = getClosestGroupId();

const draggingRecord = state.getIn(['inspector', 'dragging']);
const isFieldDrag = draggingRecord && (draggingRecord as FieldDraggingStateRecord).dsId;

return {
dragging: isFieldDrag ? draggingRecord : null,
groupId
};
}

function mapDispatchToProps(dispatch, ownProps: OwnProps): DispatchProps {
return {
facetField: (field, groupId) => {
let numCols;
if (ownProps.layoutOrientation == "Column") {
numCols = 1;
} else {
numCols = null;
}
dispatch(addFacetLayout(FacetLayout({columns: numCols})));
// dispatch(addGroupFacet(GroupFacet({facet: {name: "facet", data: "cars_source_5", groupby: [field]}}), groupId)); // remove hardcoded data name
dispatch(addFacet({name: "facet",data: "5", groupby: field} as Facet, groupId));
}
}
}

class FacetDropzone extends React.Component<StateProps & OwnProps & DispatchProps> {

public handleDragOver = (evt) => {
if (evt.preventDefault) {
evt.preventDefault();
}

return false;
};

public handleDrop = () => {
this.props.facetField(this.props.dragging.fieldDef.name, this.props.groupId);
};

public render() {
if (!this.props.dragging) return null;
return (
<div className="facet-dropzone" onDragOver={(e) => this.handleDragOver(e)} onDrop={() => this.handleDrop()}>
<div><i>Facet {this.props.layoutOrientation}</i></div>
</div>
);
}

}

export default connect(mapStateToProps, mapDispatchToProps)(FacetDropzone);
33 changes: 33 additions & 0 deletions src/js/components/pipelines/FacetOptionsHolder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react';
import { connect } from 'react-redux';
import {State} from '../../store';
import FacetDropzone from './FacetDropzone';

const layoutOrientaions = ['Row', 'Column'];
interface StateProps {
layouts: number[];
}

function mapStateToProps(state: State): StateProps {
const layoutList = state.getIn(['vis', 'present', 'layouts']);
return {
layouts: Array.from(layoutList.keys())
};
}

class FacetOptionsHolder extends React.Component<StateProps> {
public render() {

return (
<div className='facet-container'>
{layoutOrientaions.map(function(dir,i) {
return (
<FacetDropzone key={i} layoutOrientation={dir}/>
);
}, this)}
</div>
)}

}

export default connect(mapStateToProps, null)(FacetOptionsHolder);
17 changes: 16 additions & 1 deletion src/js/ctrl/export.ts
Original file line number Diff line number Diff line change
@@ -49,9 +49,20 @@ export function exporter(internal: boolean = false): Spec {
// Add interactions and widgets from store
spec = exporter.interactions(state, spec);
spec = exporter.widgets(state, spec);

if (state.getIn(['vis', 'present', 'facetLayouts']).size > 0){
spec.layout = exporter.layouts(state, int);
}
return spec;
}

exporter.layouts = function (state: State, internal: boolean) {
const facetLayouts = state.getIn(['vis', 'present', 'facetLayouts']);
const layout = clean(duplicate(facetLayouts), internal);
const id = Object.keys(layout)[Object.keys(layout).length -1];
return layout[id];
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if i'm understanding this correctly, the logic is:

  • if there are any facet layouts in the lyra store, set the last one created as the layout in the exported spec

a few questions to help my understanding:

  • why do we discard the previous ones?
  • what are the cases where there will be more than one facet layout?
  • how is this different from how non-facet layouts are handled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really wasn't sure on how the logic of the export worked so this might be something we can walk through tomorrow


exporter.interactions = function(state: State, spec) {
state.getIn(['vis', 'present', 'interactions']).forEach((interaction: InteractionRecord) => {
const group: GroupRecord = state.getIn(['vis', 'present', 'marks', String(interaction.groupId)]);
@@ -216,12 +227,16 @@ exporter.mark = function(state: State, internal: boolean, id: number) {
spec.from = {data: facet.name};
} else if (spec.from) {
let fromId;
if ((fromId = spec.from.data)) {
if ((fromId = spec.from.name)) {
spec.from = {"data": fromId};
} else if ((fromId = spec.from.data)) {
spec.from.data = name(getInVis(state, 'datasets.' + fromId + '.name'));
const count = counts.data[fromId] || (counts.data[fromId] = duplicate(DATA_COUNT));
count.marks[id] = true;
} else if ((fromId = spec.from.mark)) {
spec.from.mark = name(getInVis(state, 'marks.' + fromId + '.name'));
} else if ((fromId = spec.from.facet.data)) {
spec.from.facet.data = name(getInVis(state, 'datasets.' + fromId + '.name'));
}
}

24 changes: 24 additions & 0 deletions src/js/reducers/facetLayoutsReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Map} from 'immutable';
import {ActionType, getType} from 'typesafe-actions';
import {FacetLayoutState} from '../store/factory/FacetLayout';
import * as FacetLayoutActions from '../actions/facetLayoutActions';

/**
* This reducer handles layout updates
* @param {Object} state - An Immutable state object
* @param {Object} action - An action object
*/
export function facetLayoutsReducer(state: FacetLayoutState,
action: ActionType<typeof FacetLayoutActions>): FacetLayoutState {
const id = String(action.meta);

if (typeof state === 'undefined') {
return Map();
}

if (action.type === getType(FacetLayoutActions.baseAddFacetLayout)) {
return state.set(id, action.payload);
}

return state;
}
4 changes: 3 additions & 1 deletion src/js/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import {invalidateVegaReducer as vega} from './vegaReducer';
import {lyraGlobalsReducer as lyra} from './lyraReducer';
import {walkthroughReducer as walkthrough} from './walkthroughReducer';
import {layoutsReducer as layouts} from './layoutsReducer';
import {facetLayoutsReducer as facetLayouts} from './facetLayoutsReducer';

const visReducers = combineReducers({
signals,
@@ -28,7 +29,8 @@ const visReducers = combineReducers({
marks,
interactions,
widgets,
layouts
layouts,
facetLayouts
});

// order matters here
8 changes: 8 additions & 0 deletions src/js/reducers/marksReducer.ts
Original file line number Diff line number Diff line change
@@ -250,6 +250,14 @@ export function marksReducer(
return ensureValuePresentImmutable(state, [String(groupId), '_widgets'], action.payload);
}

if (action.type == getType(markActions.baseAddGroupFacet)) {
return state.setIn([String(groupId), "from"], {facet: action.payload});
}

if (action.type == getType(markActions.baseAddFacet)) {
return state.setIn([String(groupId), "from"], action.payload);
}

const id = action.meta;

if (action.type === getType(guideActions.deleteGuide)) {
1 change: 1 addition & 0 deletions src/js/reducers/vegaReducer.ts
Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@ export function invalidateVegaReducer(state: VegaReparseRecord,
case getType(datasetActions.sortDataset):
case getType(datasetActions.addTransform):
case getType(datasetActions.updateTransform):
case getType(markActions.baseAddFacet):
case getType(hydrate):
case historyActions.UNDO:
case historyActions.REDO:
41 changes: 41 additions & 0 deletions src/js/store/factory/FacetLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {Map, Record, RecordOf} from 'immutable';

/**
* Layouts align multiple groups
*/
export interface FacetLayout {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there enough overlap between FacetLayout and Layout that we can think about e.g.:

  • a common interface that both of them "inherit" from via the typescript & (intersection type) operator
  • a "parent type" defined as the | (union type) of the two?

i'm not suggesting either particular choice is more right here but consider whether or not that might simplify things in some places (it also might not simplify things at all, in which case feel free to reject this idea)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, they are similar in concept but very different in implementation and how they are used in Lyra and the ultimate vega spec. Perhaps worth discussing further

/**
* The Lyra ID of this vega layout.
*/
_id: number;
/**
* Number of columns in this layout.
*/
columns: number;
/**
* Spacing between groups in this layout.
*/
padding: number;
/**
* Bounds for this layout.
*/
bounds: string;
/**
* Group alignment for this layout.
*/
align: string;

}

export const FacetLayout = Record<FacetLayout>({
_id: null,
columns: null,
padding: 30,
bounds: "full",
align: "all"
}, 'FacetLayout');

export type FacetLayoutRecord = RecordOf<FacetLayout>;

export type FacetLayoutState = Map<string, FacetLayoutRecord>;

4 changes: 1 addition & 3 deletions src/js/store/factory/Mark.ts
Original file line number Diff line number Diff line change
@@ -34,11 +34,9 @@ export interface LyraMarkMeta {
_id: number;
_parent: number;
_vlUnit: LyraVegaLiteSpec;
_facet: Facet;
}

export interface LyraPathFacet {
_facet: Facet
}

export type LyraMark = LyraAreaMark | LyraGroupMark | LyraLineMark | LyraRectMark | LyraSymbolMark | LyraTextMark;
export type MarkRecord = AreaRecord | GroupRecord | LineRecord | RectRecord | SymbolRecord | TextRecord;
1 change: 1 addition & 0 deletions src/js/store/factory/marks/Area.ts
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ export const Area = Record<LyraAreaMark>({
_id: null,
_parent: null,
_vlUnit: null,
_facet: null,
type: 'area',
name: null,
from: null,
2 changes: 1 addition & 1 deletion src/js/store/factory/marks/Group.ts
Original file line number Diff line number Diff line change
@@ -48,4 +48,4 @@ export const Group = Record<LyraGroupMark>({
}
}, 'LyraGroupMark');

export type GroupRecord = RecordOf<LyraGroupMark>;
export type GroupRecord = RecordOf<LyraGroupMark>;
4 changes: 2 additions & 2 deletions src/js/store/factory/marks/Line.ts
Original file line number Diff line number Diff line change
@@ -3,10 +3,10 @@ import {LineMark} from 'vega-typings';
import anchorTarget from '../../../util/anchor-target';
import {propSg} from '../../../util/prop-signal';
import test from '../../../util/test-if';
import {HandleStreams, LyraMarkMeta, LyraPathFacet} from '../Mark';
import {HandleStreams, LyraMarkMeta} from '../Mark';
import {DELTA} from '../Signal';

export type LyraLineMark = LyraMarkMeta & LineMark & LyraPathFacet;
export type LyraLineMark = LyraMarkMeta & LineMark;

export const Line = Record<LyraLineMark>({
_id: null,
1 change: 1 addition & 0 deletions src/js/store/factory/marks/Rect.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ export const Rect = Record<LyraRectMark>({
_id: null,
_parent: null,
_vlUnit: null,
_facet: null,
type: 'rect',
name: null,
from: null,
1 change: 1 addition & 0 deletions src/js/store/factory/marks/Symbol.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ export const Symbol = Record<LyraSymbolMark>({
_id: null,
_parent: null,
_vlUnit: null,
_facet: null,
type: 'symbol',
name: null,
from: null,
1 change: 1 addition & 0 deletions src/js/store/factory/marks/Text.ts
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ export const Text = Record<LyraTextMark>({
_id: null,
_parent: null,
_vlUnit: null,
_facet: null,
type: 'text',
name: null,
from: null,
Loading