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
5 changes: 4 additions & 1 deletion src/js/actions/bindChannel/parseMarks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export default function parseMarks(dispatch: Dispatch, state: State, parsed: Com
bindProperty(dispatch, parsed, def.encode.update);
}

const parentID = state.getIn(['vis', 'present', 'marks', String(markId), '_parent']);
const isFacetMark = parentID && state.getIn(['vis', 'present', 'marks', String(parentID), 'from', 'facet']);

if (pathgroup) {
dispatch(updateMarkProperty({
property: '_facet',
Expand All @@ -52,7 +55,7 @@ export default function parseMarks(dispatch: Dispatch, state: State, parsed: Com
data: map.data[pathgroup.from.facet.data]
}
}, markId));
} else if (def.from && def.from.data) {
} else if (!isFacetMark && def.from && def.from.data) {
dispatch(updateMarkProperty({property: 'from', value: {data: map.data[def.from.data]}}, markId));
}
}
Expand Down
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
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/js/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -28,6 +29,7 @@ module.exports = ReactDOM.render(
<WidgetDropzone />
<MarkDropzoneGroup />
<MarkDropPlaceGroup />
<FacetOptionsHolder />
</div>
<Toolbar />
</div>
Expand Down
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, datasetId: number, 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, datasetId, 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: String(datasetId), 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.dragging.dsId, 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);
33 changes: 30 additions & 3 deletions src/js/ctrl/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ export function exporter(internal: boolean = false): Spec {
// Add interactions and widgets from store
spec = exporter.interactions(state, spec);
spec = exporter.widgets(state, spec);

console.log("final spec", spec);
return spec;
}


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)]);
Expand Down Expand Up @@ -216,12 +219,20 @@ 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 (spec.from.name) {
fromId = spec.from.name;
spec.from = {"data": fromId};
} else if ( spec.from.data) {
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)) {
} else if (spec.from.mark) {
fromId = spec.from.mark;
spec.from.mark = name(getInVis(state, 'marks.' + fromId + '.name'));
} else if (spec.from.facet.data) {
fromId = spec.from.facet.data;
spec.from.facet.data = name(getInVis(state, 'datasets.' + fromId + '.name'));
}
}

Expand Down Expand Up @@ -280,7 +291,7 @@ function pathgroup(state, marks, facet) {

exporter.group = function(state: State, internal: boolean, id: number) {
const mark: GroupRecord = getInVis(state, `marks.${id}`);
const spec = exporter.mark(state, internal, id);
let spec = exporter.mark(state, internal, id);
const group = internal ? spec[0] : spec;

['scale', 'mark', 'axe', 'legend'].forEach(function(childType) {
Expand Down Expand Up @@ -316,6 +327,22 @@ exporter.group = function(state: State, internal: boolean, id: number) {
);
}

if (mark.from) {
if (mark.from.facet) {
const facetLayouts = state.getIn(['vis', 'present', 'facetLayouts']).toJS();
console.log("layouts", facetLayouts);
const id = Object.keys(facetLayouts)[Object.keys(facetLayouts).length -1]; //add facet id to groups instead
console.log("id", id)
const layout = clean(facetLayouts[id], internal);
console.log("layout", layout);
spec = {
"type": "group",
"layout": layout,
"marks": spec
};
}
}

return spec;
};

Expand Down
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
Expand Up @@ -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,
Expand All @@ -28,7 +29,8 @@ const visReducers = combineReducers({
marks,
interactions,
widgets,
layouts
layouts,
facetLayouts
});

// order matters here
Expand Down
8 changes: 8 additions & 0 deletions src/js/reducers/marksReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
1 change: 1 addition & 0 deletions src/js/reducers/vegaReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
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 = FacetLayoutRecord;

4 changes: 1 addition & 3 deletions src/js/store/factory/Mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/js/store/factory/marks/Area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const Area = Record<LyraAreaMark>({
_id: null,
_parent: null,
_vlUnit: null,
_facet: null,
type: 'area',
name: null,
from: null,
Expand Down
2 changes: 1 addition & 1 deletion src/js/store/factory/marks/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ export const Group = Record<LyraGroupMark>({
}
}, 'LyraGroupMark');

export type GroupRecord = RecordOf<LyraGroupMark>;
export type GroupRecord = RecordOf<LyraGroupMark>;
Loading