Skip to content

Commit

Permalink
refactor: more splitting of modules
Browse files Browse the repository at this point in the history
  • Loading branch information
sgratzl committed Sep 22, 2021
1 parent cac4f48 commit a08ffd3
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 123 deletions.
10 changes: 10 additions & 0 deletions src/_polyfills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @upsetjs/r
* https://github.com/upsetjs/upsetjs_r
*
* Copyright (c) 2021 Samuel Gratzl <sam@sgratzl.com>
*/

import 'core-js';
import 'element-closest-polyfill';
import 'regenerator-runtime/runtime';
159 changes: 40 additions & 119 deletions src/htmlwidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,10 @@

/// <reference path="../types/index.d.ts" />

import { RBindingUpSetProps, Elem, adapter, syncAddons, UpSetAttrSpec } from './model';
import {
isElemQuery,
ISetCombinations,
ISetLike,
isSetQuery,
render,
UpSetProps,
renderVennDiagram,
renderKarnaughMap,
VennDiagramProps,
KarnaughMapProps,
ISets,
} from '@upsetjs/bundle';
import { fixCombinations, fixSets, resolveSet, resolveSetByElems, fromExpression } from './utils';
import './_polyfills';
import { ISetCombinations, ISetLike, render, renderKarnaughMap, renderVennDiagram } from '@upsetjs/bundle';
import { adapter, createContext, Elem, fixProps, RBindingUpSetProps } from './model';
import { resolveSetByElems } from './utils';

declare type CrosstalkOptions = {
group: string;
Expand All @@ -38,132 +27,62 @@ declare type CrosstalkHandler = {
trigger(elems?: ReadonlyArray<string>): void;
};

function isShinyMode(): boolean {
return HTMLWidgets && HTMLWidgets.shinyMode;
}

HTMLWidgets.widget({
name: 'upsetjs',
type: 'output',

factory(el, width, height) {
let interactive = false;
let renderMode: 'upset' | 'venn' | 'euler' | 'kmap' = 'upset';
const elemToIndex = new Map<Elem, number>();
let attrs: UpSetAttrSpec[] = [];
const props: UpSetProps<Elem> & VennDiagramProps<Elem> & KarnaughMapProps<Elem> = {
sets: [],
width,
height,
exportButtons: HTMLWidgets.shinyMode,
};
const context = createContext(width, height, {
exportButtons: isShinyMode(),
});
let crosstalkHandler: CrosstalkHandler | null = null;

function fixProps(props: UpSetProps<Elem> & VennDiagramProps<Elem>, delta: any) {
if (typeof delta.interactive === 'boolean') {
interactive = delta.interactive;
}
const expressionData = delta.expressionData;
if (typeof delta.renderMode === 'string') {
renderMode = delta.renderMode;
}
delete (props as any).renderMode;
delete (props as any).interactive;
delete (props as any).expressionData;
delete (props as any).crosstalk;
if (delta.elems) {
// elems = delta.elems;
elemToIndex.clear();
delta.elems.forEach((elem: Elem, i: number) => elemToIndex.set(elem, i));
}
delete (props as any).elems;
if (delta.attrs) {
attrs = delta.attrs;
syncAddons(props, elemToIndex, attrs);
}
delete (props as any).attrs;

if (delta.sets != null) {
props.sets = fixSets(props.sets);
}
if (delta.combinations != null) {
if (expressionData) {
const r = fromExpression(delta.combinations);
props.combinations = r.combinations as ISetCombinations<string>;
props.sets = r.sets as ISets<string>;
} else {
const c = fixCombinations(delta.combinations, props.sets);
if (c == null) {
delete props.combinations;
} else {
props.combinations = c;
}
}
}
if (typeof delta.selection === 'string' || Array.isArray(delta.selection)) {
props.selection = resolveSet(delta.selection, props.sets, props.combinations as ISetCombinations<Elem>);
}
props.onHover = interactive || HTMLWidgets.shinyMode ? onHover : undefined;

if (delta.queries) {
props.queries = delta.queries.map((query: any) => {
const base = Object.assign({}, query);
if (isSetQuery(query) && (typeof query.set === 'string' || Array.isArray(query.set))) {
base.set = resolveSet(query.set, props.sets, props.combinations as ISetCombinations<Elem>)!;
} else if (isElemQuery(query) && typeof query.elems !== 'undefined' && !Array.isArray(query.elems)) {
base.elems = [query.elems];
}
return base;
});
}
}

function update(delta?: any, append = false) {
if (delta) {
if (append) {
Object.keys(delta).forEach((key) => {
const p = props as any;
const old = p[key] || [];
p[key] = old.concat(delta[key]);
});
} else {
Object.assign(props, delta);
}
fixProps(props, delta);
fixProps(context, delta, append);
context.props.onHover = context.interactive || isShinyMode() ? onHover : undefined;
}
if (renderMode === 'venn') {
delete props.layout;
renderVennDiagram(el, props);
} else if (renderMode === 'kmap') {
renderKarnaughMap(el, props);
} else if (renderMode === 'euler') {
props.layout = adapter;
renderVennDiagram(el, props);
if (context.renderMode === 'venn') {
delete context.props.layout;
renderVennDiagram(el, context.props);
} else if (context.renderMode === 'kmap') {
renderKarnaughMap(el, context.props);
} else if (context.renderMode === 'euler') {
context.props.layout = adapter;
renderVennDiagram(el, context.props);
} else {
render(el, props);
render(el, context.props);
}
}

let bakSelection: ISetLike<Elem> | null | undefined | ReadonlyArray<Elem> | ((s: ISetLike<string>) => number) =
null;

const onHover = (set: ISetLike<Elem> | null) => {
if (HTMLWidgets.shinyMode) {
if (isShinyMode()) {
Shiny.onInputChange(`${el.id}_hover`, {
name: set ? set.name : null,
elems: set ? set.elems || [] : [],
});
}
const crosstalk = crosstalkHandler && crosstalkHandler.mode === 'hover';
if (!interactive && !crosstalk) {
if (!context.interactive && !crosstalk) {
return;
}
if (crosstalk && crosstalkHandler) {
crosstalkHandler.trigger(set?.elems as string[]);
}
if (set) {
// hover on
bakSelection = props.selection;
props.selection = set;
bakSelection = context.props.selection;
context.props.selection = set;
} else {
// hover off
props.selection = bakSelection;
context.props.selection = bakSelection;
bakSelection = null;
}
update();
Expand All @@ -176,16 +95,18 @@ HTMLWidgets.widget({
if (event.sender === sel) {
return;
}
props.selection = !event.value
context.props.selection = !event.value
? null
: resolveSetByElems(event.value, props.sets, props.combinations as ISetCombinations<Elem>) || event.value;
: resolveSetByElems(event.value, context.props.sets, context.props.combinations as ISetCombinations<Elem>) ||
event.value;
update();
});

// show current state
props.selection = !sel.value
context.props.selection = !sel.value
? null
: resolveSetByElems(sel.value, props.sets, props.combinations as ISetCombinations<Elem>) || sel.value;
: resolveSetByElems(sel.value, context.props.sets, context.props.combinations as ISetCombinations<Elem>) ||
sel.value;
update();

return {
Expand All @@ -204,28 +125,28 @@ HTMLWidgets.widget({
};
}

if (HTMLWidgets.shinyMode) {
props.onClick = (set: ISetLike<Elem> | null) => {
if (isShinyMode()) {
context.props.onClick = (set: ISetLike<Elem> | null) => {
Shiny.onInputChange(`${el.id}_click`, {
name: set ? set.name : null,
elems: set ? set.elems || [] : [],
});

if (crosstalkHandler && crosstalkHandler.mode === 'click') {
crosstalkHandler.trigger(set?.elems);
props.selection = set;
context.props.selection = set;
update();
}
};
props.onContextMenu = (set: ISetLike<Elem> | null) => {
context.props.onContextMenu = (set: ISetLike<Elem> | null) => {
Shiny.onInputChange(`${el.id}_contextMenu`, {
name: set ? set.name : null,
elems: set ? set.elems || [] : [],
});

if (crosstalkHandler && crosstalkHandler.mode === 'contextMenu') {
crosstalkHandler.trigger(set?.elems);
props.selection = set;
context.props.selection = set;
update();
}
};
Expand All @@ -236,7 +157,7 @@ HTMLWidgets.widget({
return {
renderValue(x: ShinyUpSetProps) {
update(x);
if (x.crosstalk && (window as any).crosstalk && HTMLWidgets.shinyMode) {
if (x.crosstalk && (window as any).crosstalk && isShinyMode()) {
if (!crosstalkHandler) {
crosstalkHandler = enableCrosstalk(x.crosstalk);
} else {
Expand All @@ -254,7 +175,7 @@ HTMLWidgets.widget({
},
});

if (HTMLWidgets.shinyMode) {
if (isShinyMode()) {
Shiny.addCustomMessageHandler('upsetjs-update', (msg) => {
const el = document.getElementById(msg.id);
const update: (props: any, append: boolean) => void = (el as any)?.__update;
Expand Down
107 changes: 103 additions & 4 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@
*
* Copyright (c) 2021 Samuel Gratzl <sam@sgratzl.com>
*/

import {
boxplotAddon,
categoricalAddon,
createVennJSAdapter,
isElemQuery,
ISetCombinations,
ISets,
isSetQuery,
KarnaughMapProps,
UpSetProps,
VennDiagramProps,
} from '@upsetjs/bundle';
import { layout } from '@upsetjs/venn.js';
import 'core-js';
import 'element-closest-polyfill';
import 'regenerator-runtime/runtime';
import { fixCombinations, fixSets, fromExpression, resolveSet } from './utils';

export declare type Elem = string;

Expand Down Expand Up @@ -86,3 +87,101 @@ export function syncAddons(
props.setAddons = attrs.map((attr) => toAddon(attr, false));
props.combinationAddons = attrs.map((attr) => toAddon(attr, true));
}

export interface RenderContext {
props: UpSetProps<Elem> & VennDiagramProps<Elem> & KarnaughMapProps<Elem>;
elemToIndex: Map<Elem, number>;
attrs: UpSetAttrSpec[];
interactive: boolean;
renderMode: 'upset' | 'venn' | 'euler' | 'kmap';
}

export function createContext(
width: number,
height: number,
extra: Partial<RenderContext['props']> = {}
): RenderContext {
return {
interactive: false,
renderMode: 'upset',
elemToIndex: new Map<Elem, number>(),
attrs: [],
props: {
sets: [],
width,
height,
...extra,
},
};
}

export function fixProps(context: RenderContext, delta: any, append = false) {
if (append) {
Object.keys(delta).forEach((key) => {
const p = context.props as any;
const old = p[key] || [];
p[key] = old.concat(delta[key]);
});
} else {
Object.assign(context.props, delta);
}

if (typeof delta.interactive === 'boolean') {
context.interactive = delta.interactive;
}
const expressionData = delta.expressionData;
if (typeof delta.renderMode === 'string') {
context.renderMode = delta.renderMode;
}
delete (context.props as any).renderMode;
delete (context.props as any).interactive;
delete (context.props as any).expressionData;
delete (context.props as any).crosstalk;
if (delta.elems) {
// elems = delta.elems;
context.elemToIndex.clear();
delta.elems.forEach((elem: Elem, i: number) => context.elemToIndex.set(elem, i));
}
delete (context.props as any).elems;
if (delta.attrs) {
context.attrs = delta.attrs;
syncAddons(context.props, context.elemToIndex, context.attrs);
}
delete (context.props as any).attrs;

if (delta.sets != null) {
context.props.sets = fixSets(context.props.sets);
}
if (delta.combinations != null) {
if (expressionData) {
const r = fromExpression(delta.combinations);
context.props.combinations = r.combinations as ISetCombinations<string>;
context.props.sets = r.sets as ISets<string>;
} else {
const c = fixCombinations(delta.combinations, context.props.sets);
if (c == null) {
delete context.props.combinations;
} else {
context.props.combinations = c;
}
}
}
if (typeof delta.selection === 'string' || Array.isArray(delta.selection)) {
context.props.selection = resolveSet(
delta.selection,
context.props.sets,
context.props.combinations as ISetCombinations<Elem>
);
}
if (delta.queries) {
context.props.queries = delta.queries.map((query: any) => {
const base = Object.assign({}, query);
if (isSetQuery(query) && (typeof query.set === 'string' || Array.isArray(query.set))) {
base.set = resolveSet(query.set, context.props.sets, context.props.combinations as ISetCombinations<Elem>)!;
} else if (isElemQuery(query) && typeof query.elems !== 'undefined' && !Array.isArray(query.elems)) {
base.elems = [query.elems];
}
return base;
});
}
}

0 comments on commit a08ffd3

Please sign in to comment.