Skip to content

Commit

Permalink
Added zoomkey method
Browse files Browse the repository at this point in the history
  • Loading branch information
alexsocha committed Jan 11, 2019
1 parent 0692c46 commit 3a50ad5
Show file tree
Hide file tree
Showing 22 changed files with 103 additions and 59 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -12,7 +12,8 @@
<img src="https://raw.githubusercontent.com/algrx/algorithmx/master/img/example.svg?sanitize=true" align="center" width="600px">

## Resources
- <a href='https://algrx.github.io/algorithmx/docs/js'>Docs</a>
- <a href='https://algrx.github.io/algorithmx/docs/js'>Documentation</a>
- <a href="https://github.com/algrx/algorithmx-python">Python version</a>

## Installing

Expand Down
2 changes: 1 addition & 1 deletion src/client/attributes/definitions/animation.ts
Expand Up @@ -52,7 +52,7 @@ export const defaults: IAnimation = {
type: 'normal',
duration: 0.35,
ease: 'poly',
linger: 1
linger: 0.5
}

export const createFullDef = <T extends Attr, A extends Attr>(bodyDef: AttrDef<T>, endDef: AttrDef<A>):
Expand Down
7 changes: 5 additions & 2 deletions src/client/attributes/definitions/canvas.ts
@@ -1,4 +1,4 @@
import { PartialAttr, AttrLookup, AttrNum, AttrString, EnumVarSymbol, AttrEvalPartial } from '../types'
import { PartialAttr, AttrLookup, AttrNum, AttrString, AttrBool, EnumVarSymbol, AttrEvalPartial } from '../types'
import { AnimationFull } from './animation'
import { IElementAttr, ISvgMixinAttr } from './element'
import { ILabelAttr } from './label'
Expand Down Expand Up @@ -47,6 +47,7 @@ export interface ICanvasAttr extends IElementAttr, ISvgMixinAttr {
readonly min: AttrNum
readonly max: AttrNum
}
readonly zoomkey: AttrBool
}

export const definition = attrDef.extendRecordDef<ICanvasAttr, IElementAttr>({
Expand Down Expand Up @@ -76,9 +77,10 @@ export const definition = attrDef.extendRecordDef<ICanvasAttr, IElementAttr>({
min: { type: AttrType.Number },
max: { type: AttrType.Number }
}, keyOrder: ['min', 'max'] },
zoomkey: { type: AttrType.Boolean },
...attrElement.svgMixinDefEntries
},
keyOrder: ['nodes', 'edges', 'labels', 'size', 'edgelengths', 'pan', 'zoom', 'panlimit', 'zoomlimit',
keyOrder: ['nodes', 'edges', 'labels', 'size', 'edgelengths', 'pan', 'zoom', 'panlimit', 'zoomlimit', 'zoomkey',
...attrElement.svgMixinDefKeys],
validVars: [EnumVarSymbol.CanvasWidth, EnumVarSymbol.CanvasHeight]
}, attrElement.definition)
Expand All @@ -97,6 +99,7 @@ export const defaults: ICanvasAttr = {
zoom: 1,
panlimit: { horizontal: Infinity, vertical: Infinity },
zoomlimit: { min: 0.1, max: 10 },
zoomkey: false,
...attrElement.svgMixinDefaults
}

Expand Down
11 changes: 9 additions & 2 deletions src/client/client.ts
Expand Up @@ -76,13 +76,20 @@ export const client = (canvas: Canvas): Client => {
task.execute()
},
executeEvent: event => {
const state = clientEvents.executeEvent(self().state, self().listener, event)
const executeContext: clientEvents.ExecuteContext = {
state: self().state,
listener: self().listener,
tick: self().tick
}
const state = clientEvents.executeEvent(executeContext, event)
self().setState(state)
self().tick()
},

tick: () => {
const state = self().state
renderCanvasLive.updateCanvas(canvas, state.attributes, state.layout)
if (state !== undefined && state.attributes !== undefined)
renderCanvasLive.updateCanvas(canvas, state.attributes, state.layout)
}
})

Expand Down
58 changes: 31 additions & 27 deletions src/client/events.ts
Expand Up @@ -12,6 +12,12 @@ import * as renderCanvasLive from './render/canvas/live'
import * as renderCanvasMisc from './render/canvas/misc'
import * as layout from './layout/layout'

export interface ExecuteContext {
readonly state: IClientState,
readonly listener: ClientListener,
readonly tick: () => void
}

export const dispatchError = (message: string, type: events.EnumErrorType): events.IReceiveError =>
({ type: events.EnumReceiveType.error, data: { message: message, type: type } })

Expand All @@ -21,13 +27,13 @@ const dispatchClick = (nodeId: string): events.IReceiveClick =>
const dispatchHover = (nodeId: string, entered: boolean): events.IReceiveHover =>
({ type: events.EnumReceiveType.hover, data: { id: nodeId, entered: entered } })

const executeReset = (state: IClientState, listener: ClientListener,
event: events.IDispatchUpdate): IClientState => {
const executeReset = (context: ExecuteContext, event: events.IDispatchUpdate): IClientState => {
const state = context.state
if (state.attributes === undefined) return state

const processed = pipeline.processReset(state.attributes, event.data)
if (processed instanceof Error) {
listener(dispatchError(processed.message, events.EnumErrorType.attribute))
context.listener(dispatchError(processed.message, events.EnumErrorType.attribute))
return state
}

Expand All @@ -43,15 +49,14 @@ const executeReset = (state: IClientState, listener: ClientListener,
}

const render = (canvas: events.Canvas, renderData: RenderAttr<ICanvasAttr>,
layoutState: layout.ILayoutState): void => {
tick: () => void, layoutState: layout.ILayoutState): void => {
renderCanvas.renderCanvas(canvas, renderData)
if (renderData.attr.visible === false) return

renderCanvasMisc.renderLayout(canvas, renderData, layoutState)
renderCanvasMisc.renderWithLayout(canvas, renderData, layoutState)

const updateLiveFn = () => renderCanvasLive.updateCanvas(canvas, renderData.attr, layoutState)
renderCanvasMisc.renderWithLiveUpdate(canvas, renderData, updateLiveFn)
updateLiveFn()
renderCanvasMisc.renderWithTick(canvas, renderData, tick)
renderCanvasLive.updateCanvas(canvas, renderData.attr, layoutState)
}

const renderBehavior = (canvas: events.Canvas, renderData: RenderAttr<ICanvasAttr>,
Expand All @@ -64,25 +69,25 @@ const renderBehavior = (canvas: events.Canvas, renderData: RenderAttr<ICanvasAtt
return newBehavior
}

const executeUpdate = (state: IClientState, listener: ClientListener,
event: events.IDispatchUpdate): IClientState => {
if (event.data.attributes === null) return executeReset(state, listener, event)
const executeUpdate = (context: ExecuteContext, event: events.IDispatchUpdate): IClientState => {
const state = context.state
if (event.data.attributes === null) return executeReset(context, event)

const processed = pipeline.processUpdate(state.canvas, state.attributes, state.expressions, event.data)
if (processed instanceof Error) {
listener(dispatchError(processed.message, events.EnumErrorType.attribute))
context.listener(dispatchError(processed.message, events.EnumErrorType.attribute))
return state
}

const renderData = renderElement.preprocess(pipeline.getRenderData(processed))
const layoutState = layout.update(state.layout, processed.attributes, processed.changes)

render(state.canvas, renderData, layoutState)
render(state.canvas, renderData, context.tick, layoutState)
const newBehavior = renderBehavior(state.canvas, renderData, state.renderBehavior)

if (processed.attributes.visible) {
const clickFn = (n: string) => listener(dispatchClick(n))
const hoverFn = (n: string, h: boolean) => listener(dispatchHover(n, h))
const clickFn = (n: string) => context.listener(dispatchClick(n))
const hoverFn = (n: string, h: boolean) => context.listener(dispatchHover(n, h))
renderCanvasListeners.registerNodeClick(state.canvas, renderData, clickFn)
renderCanvasListeners.registerNodeHover(state.canvas, renderData, hoverFn)
}
Expand All @@ -95,11 +100,11 @@ const executeUpdate = (state: IClientState, listener: ClientListener,
}
}

const executeHighlight = (state: IClientState, listener: ClientListener,
event: events.IDispatchHighlight): void => {
const executeHighlight = (context: ExecuteContext, event: events.IDispatchHighlight): void => {
const state = context.state
const processed = pipeline.processHighlight(state.attributes, state.expressions, event.data)
if (processed instanceof Error) {
listener(dispatchError(processed.message, events.EnumErrorType.attribute))
context.listener(dispatchError(processed.message, events.EnumErrorType.attribute))
return
}

Expand All @@ -111,25 +116,24 @@ const executeHighlight = (state: IClientState, listener: ClientListener,
}
const renderData = renderElement.preprocess(renderDataInit)

render(state.canvas, renderData, state.layout)
render(state.canvas, renderData, context.tick, state.layout)
renderBehavior(state.canvas, renderData, state.renderBehavior)
}

export const executeEvent = (state: IClientState, listener: ClientListener,
event: events.DispatchEvent): IClientState => {
export const executeEvent = (context: ExecuteContext, event: events.DispatchEvent): IClientState => {
if (event.type === events.EnumDispatchType.broadcast) {
listener({
context.listener({
type: events.EnumReceiveType.broadcast,
data: { message: event.data.message }
})
return state
return context.state

} else if (event.type === events.EnumDispatchType.update) {
return executeUpdate(state, listener, event)
return executeUpdate(context, event)

} else if (event.type === events.EnumDispatchType.highlight) {
executeHighlight(state, listener, event)
return state
executeHighlight(context, event)
return context.state

} else return state
} else return context.state
}
16 changes: 14 additions & 2 deletions src/client/render/canvas/behavior.ts
Expand Up @@ -16,19 +16,31 @@ const updatePanZoomLimit = (selection: D3Selection, renderData: RenderAttr<ICanv
behavior: RenderBehavior['zoom'] | undefined): RenderBehavior['zoom'] => {
if (renderProcess.hasChanged(getEntry(renderData, 'zoomlimit'))
|| renderProcess.hasChanged(getEntry(renderData, 'panlimit'))
|| renderProcess.hasChanged(getEntry(renderData, 'zoomkey'))
|| behavior === undefined) {

const onZoom = () => canvasUtils.selectCanvasInner(selection)
.attr('transform', d3.event ? d3.event.transform : '')
const onZoom = () => {
canvasUtils.selectCanvasInner(selection)
.attr('transform', d3.event ? d3.event.transform : '')
}

const zoomFilter = (requiresKey: boolean) => {
if (d3.event && d3.event.type === 'wheel' && requiresKey) {
return d3.event.ctrlKey || d3.event.metaKey
} else return true
}

const panH = renderData.attr.panlimit.horizontal
const panV = renderData.attr.panlimit.vertical
const zoomKey = renderData.attr.zoomkey
const newBehavior = d3.zoom()
.translateExtent([[-panH, -panV], [panH, panV]])
.scaleExtent([renderData.attr.zoomlimit.min, renderData.attr.zoomlimit.max])
.on('zoom', onZoom)
.filter(() => zoomFilter(zoomKey))

selection.call(newBehavior)

return newBehavior

} else return behavior
Expand Down
11 changes: 6 additions & 5 deletions src/client/render/canvas/misc.ts
Expand Up @@ -10,7 +10,8 @@ import * as renderElement from '../element'
import * as renderNode from '../node/render'
import * as renderNodeDrag from '../node/drag'

export const renderLayout = (canvas: Canvas, renderData: RenderAttr<ICanvasAttr>, layoutState: ILayoutState): void => {
export const renderWithLayout = (canvas: Canvas, renderData: RenderAttr<ICanvasAttr>,
layoutState: ILayoutState): void => {
const canvasSel = canvasUtils.selectCanvas(canvas)
const nodeGroup = canvasUtils.selectNodeGroup(canvasUtils.selectCanvasInner(canvasSel))

Expand All @@ -23,8 +24,8 @@ export const renderLayout = (canvas: Canvas, renderData: RenderAttr<ICanvasAttr>
})
}

export const renderWithLiveUpdate = (canvas: Canvas, renderData: RenderAttr<ICanvasAttr>,
liveUpdate: () => void): void => {
export const renderWithTick = (canvas: Canvas, renderData: RenderAttr<ICanvasAttr>,
tick: () => void): void => {
// changing node size requires the live layout function to be called continuously,
// so that connected edges are animated as well
const canvasSel = canvasUtils.selectCanvas(canvas)
Expand All @@ -41,12 +42,12 @@ export const renderWithLiveUpdate = (canvas: Canvas, renderData: RenderAttr<ICan

renderFns.render(selection, width, (liveSel, w) => {
if (renderUtils.isTransition(liveSel))
return liveSel.attr('_width', w).tween(name, () => () => { liveUpdate() })
return liveSel.attr('_width', w).tween(name, () => () => { tick() })
else return liveSel.attr('_width', w)
})
renderFns.render(selection, height, (liveSel, h) => {
if (renderUtils.isTransition(liveSel))
return liveSel.attr('_height', h).tween(name, () => () => { liveUpdate() })
return liveSel.attr('_height', h).tween(name, () => () => { tick() })
else return liveSel.attr('_height', h)
})
})
Expand Down
6 changes: 5 additions & 1 deletion src/client/render/canvas/render.ts
Expand Up @@ -2,6 +2,7 @@ import { Canvas } from '../../types/events'
import { ICanvasAttr } from '../../attributes/definitions/canvas'
import { RenderAttr } from '../process'
import { getEntry } from '../process'
import * as renderProcess from '../process'
import * as renderFns from '../render'
import * as renderUtils from '../utils'
import * as canvasUtils from './utils'
Expand Down Expand Up @@ -35,7 +36,10 @@ const render: renderFns.RenderAttrFn<ICanvasAttr> = (selection, renderData) => {
renderElement.renderElementLookup(k => canvasUtils.selectLabel(labelGroup, k), getEntry(renderData, 'labels'),
renderLabel.render, renderLabel.renderVisible)

renderElement.renderSvgMixin(selection, renderData)
// re-render svg attributes when size changes
const updatedRenderData = renderProcess.hasChanged(getEntry(renderData, 'size'))
? renderProcess.markKeysForUpdate(renderData, ['svgattr']) : renderData
renderElement.renderSvgMixinAttr(selection, updatedRenderData)
}

export function renderCanvas (canvas: Canvas, renderData: RenderAttr<ICanvasAttr>): void {
Expand Down
2 changes: 1 addition & 1 deletion src/client/render/edge/render.ts
Expand Up @@ -87,5 +87,5 @@ export const render: renderFns.RenderAttrFn<IEdgeAttr> = (selection, renderData)
const overlaySelector = () => edgeColor.selectOverlay(edgeSel, edgeRenderId)
edgeColor.renderColor(pathSel, markerTarget, overlaySelector, renderData)

renderElement.renderSvgMixin(pathSel, renderData)
renderElement.renderSvgMixinAttr(pathSel, renderData)
}
2 changes: 1 addition & 1 deletion src/client/render/element.ts
Expand Up @@ -23,7 +23,7 @@ export const renderSvgAttr = <T extends Attr>(selection: D3Selection, key: strin
})
}

export const renderSvgMixin: renderFns.RenderAttrFn<ISvgMixinAttr> = (selection, renderData) => {
export const renderSvgMixinAttr: renderFns.RenderAttrFn<ISvgMixinAttr> = (selection, renderData) => {
const precessKey = (sel, key) => [
key.includes('@') ? sel.selectAll(key.split('@')[1]) : sel,
key.includes('@') ? key.split('@')[0] : key
Expand Down
2 changes: 1 addition & 1 deletion src/client/render/label/render.ts
Expand Up @@ -104,5 +104,5 @@ export const render: renderFns.RenderAttrFn<ILabelAttr> = (selection, renderData
renderElement.renderSvgAttr(textSel, 'font-family', v => v, getEntry(renderData, 'font'))
renderElement.renderSvgAttr(textSel, 'font-size', v => v, getEntry(renderData, 'size'))

renderElement.renderSvgMixin(textSel, renderData)
renderElement.renderSvgMixinAttr(textSel, renderData)
}
2 changes: 1 addition & 1 deletion src/client/render/node/render.ts
Expand Up @@ -77,5 +77,5 @@ export const render: renderFns.RenderAttrFn<INodeAttr> = (selection, renderDataI
renderElement.renderSvgAttr(shapeSelection, 'ry', v => v, {...cornerData, name: cornerData.name + '-y' })
}

renderElement.renderSvgMixin(shapeSelection, renderData)
renderElement.renderSvgMixinAttr(shapeSelection, renderData)
}
9 changes: 7 additions & 2 deletions src/server/CanvasSelection.ts
Expand Up @@ -12,7 +12,7 @@ import * as utils from './utils'

const receiveHandler = (event: events.ReceiveEvent, listeners: selection.SelListeners): void => {
if (event.type === events.EnumReceiveType.broadcast)
selection.triggerListener(listeners, event.data.message)
selection.triggerListener(listeners, event.data.message)

else if (event.type === events.EnumReceiveType.click)
selection.triggerListener(listeners, `click-node-${event.data.id}`)
Expand Down Expand Up @@ -82,7 +82,12 @@ const builder: ClassBuilder<CanvasSelection, ISelContext<InputCanvasAttr>> = (co
context.client.dispatch(utils.attrEvent(context, limit, d => ({ zoomlimit: d })))
return self()
},
...(selection.svgMixinBuilder(context, self))

zoomkey: required => {
context.client.dispatch(utils.attrEvent(context, required, d => ({ zoomkey: d })))
return self()
},
...(selection.svgMixinAttrBuilder(context, self))

}, selection.builder(context, self, construct))

Expand Down
2 changes: 1 addition & 1 deletion src/server/EdgeSelection.ts
Expand Up @@ -44,7 +44,7 @@ const builder: ClassBuilder<EdgeSelection, ISelContext<InputEdgeAttr>> = (contex
context.client.dispatch(utils.attrEvent(context, path, d => ({ path: d })))
return self()
},
...(selection.svgMixinBuilder(context, self))
...(selection.svgMixinAttrBuilder(context, self))

}, selection.builder(context, self, construct))

Expand Down
2 changes: 1 addition & 1 deletion src/server/LabelSelection.ts
Expand Up @@ -45,7 +45,7 @@ const builder: ClassBuilder<LabelSelection, ISelContext<InputLabelAttr>> = (cont
context.client.dispatch(utils.attrEvent(context, size, d => ({ size: d })))
return self()
},
...(selection.svgMixinBuilder(context, self))
...(selection.svgMixinAttrBuilder(context, self))

}, selection.builder(context, self, construct))

Expand Down
2 changes: 1 addition & 1 deletion src/server/NodeSelection.ts
Expand Up @@ -65,7 +65,7 @@ const builder: ClassBuilder<NodeSelection, ISelContext<InputNodeAttr>> = (contex
})
return self()
},
...(selection.svgMixinBuilder(context, self))
...(selection.svgMixinAttrBuilder(context, self))

}, selection.builder(context, self, construct))

Expand Down
2 changes: 1 addition & 1 deletion src/server/Selection.ts
Expand Up @@ -161,7 +161,7 @@ export const builder: ClassBuilder<Selection<InputElementAttr>, ISelContext<Inpu
}
})

export const svgMixinBuilder = <T extends InputSvgMixinAttr & InputElementAttr, S extends Selection<T>>
export const svgMixinAttrBuilder = <T extends InputSvgMixinAttr & InputElementAttr, S extends Selection<T>>
(context: ISelContext<T>, self: () => S) => ({

svgattr: (key: string, value: ElementArg<string | number | null>) => {
Expand Down

0 comments on commit 3a50ad5

Please sign in to comment.