Skip to content
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
Copy Markdown
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, 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));
Comment thread
ktbacher marked this conversation as resolved.
Outdated
}
}
}

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);
Comment thread
ktbacher marked this conversation as resolved.
Outdated
};

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
Expand Up @@ -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
Copy Markdown
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
Copy Markdown
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)]);
Expand Down Expand Up @@ -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)) {
Comment thread
ktbacher marked this conversation as resolved.
Outdated
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'));
}
}

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
Copy Markdown
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
Copy Markdown
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
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>;
4 changes: 2 additions & 2 deletions src/js/store/factory/marks/Line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/js/store/factory/marks/Rect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const Rect = Record<LyraRectMark>({
_id: null,
_parent: null,
_vlUnit: null,
_facet: null,
type: 'rect',
name: null,
from: null,
Expand Down
1 change: 1 addition & 0 deletions src/js/store/factory/marks/Symbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const Symbol = Record<LyraSymbolMark>({
_id: null,
_parent: null,
_vlUnit: null,
_facet: null,
type: 'symbol',
name: null,
from: null,
Expand Down
Loading