diff --git a/README.md b/README.md
index 44ea7c0..f760891 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,8 @@
## Resources
- - Docs
+ - Documentation
+ - Python version
## Installing
diff --git a/src/client/attributes/definitions/animation.ts b/src/client/attributes/definitions/animation.ts
index d11e5a0..2fb65c7 100644
--- a/src/client/attributes/definitions/animation.ts
+++ b/src/client/attributes/definitions/animation.ts
@@ -52,7 +52,7 @@ export const defaults: IAnimation = {
type: 'normal',
duration: 0.35,
ease: 'poly',
- linger: 1
+ linger: 0.5
}
export const createFullDef = (bodyDef: AttrDef, endDef: AttrDef):
diff --git a/src/client/attributes/definitions/canvas.ts b/src/client/attributes/definitions/canvas.ts
index 612d7df..8b2d121 100644
--- a/src/client/attributes/definitions/canvas.ts
+++ b/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'
@@ -47,6 +47,7 @@ export interface ICanvasAttr extends IElementAttr, ISvgMixinAttr {
readonly min: AttrNum
readonly max: AttrNum
}
+ readonly zoomkey: AttrBool
}
export const definition = attrDef.extendRecordDef({
@@ -76,9 +77,10 @@ export const definition = attrDef.extendRecordDef({
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)
@@ -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
}
diff --git a/src/client/client.ts b/src/client/client.ts
index 3b9ade6..14120a1 100644
--- a/src/client/client.ts
+++ b/src/client/client.ts
@@ -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)
}
})
diff --git a/src/client/events.ts b/src/client/events.ts
index b184e19..bfda9bd 100644
--- a/src/client/events.ts
+++ b/src/client/events.ts
@@ -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 } })
@@ -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
}
@@ -43,15 +49,14 @@ const executeReset = (state: IClientState, listener: ClientListener,
}
const render = (canvas: events.Canvas, renderData: RenderAttr,
- 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,
@@ -64,25 +69,25 @@ const renderBehavior = (canvas: events.Canvas, renderData: RenderAttr {
- 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)
}
@@ -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
}
@@ -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
}
diff --git a/src/client/render/canvas/behavior.ts b/src/client/render/canvas/behavior.ts
index 696db82..e304a53 100644
--- a/src/client/render/canvas/behavior.ts
+++ b/src/client/render/canvas/behavior.ts
@@ -16,19 +16,31 @@ const updatePanZoomLimit = (selection: D3Selection, renderData: RenderAttr {
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
diff --git a/src/client/render/canvas/misc.ts b/src/client/render/canvas/misc.ts
index 420ccbb..d186d89 100644
--- a/src/client/render/canvas/misc.ts
+++ b/src/client/render/canvas/misc.ts
@@ -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, layoutState: ILayoutState): void => {
+export const renderWithLayout = (canvas: Canvas, renderData: RenderAttr,
+ layoutState: ILayoutState): void => {
const canvasSel = canvasUtils.selectCanvas(canvas)
const nodeGroup = canvasUtils.selectNodeGroup(canvasUtils.selectCanvasInner(canvasSel))
@@ -23,8 +24,8 @@ export const renderLayout = (canvas: Canvas, renderData: RenderAttr
})
}
-export const renderWithLiveUpdate = (canvas: Canvas, renderData: RenderAttr,
- liveUpdate: () => void): void => {
+export const renderWithTick = (canvas: Canvas, renderData: RenderAttr,
+ 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)
@@ -41,12 +42,12 @@ export const renderWithLiveUpdate = (canvas: Canvas, renderData: RenderAttr {
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)
})
})
diff --git a/src/client/render/canvas/render.ts b/src/client/render/canvas/render.ts
index 17f3ed6..db9b0fb 100644
--- a/src/client/render/canvas/render.ts
+++ b/src/client/render/canvas/render.ts
@@ -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'
@@ -35,7 +36,10 @@ const render: renderFns.RenderAttrFn = (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): void {
diff --git a/src/client/render/edge/render.ts b/src/client/render/edge/render.ts
index 68bd2eb..62823ed 100644
--- a/src/client/render/edge/render.ts
+++ b/src/client/render/edge/render.ts
@@ -87,5 +87,5 @@ export const render: renderFns.RenderAttrFn = (selection, renderData)
const overlaySelector = () => edgeColor.selectOverlay(edgeSel, edgeRenderId)
edgeColor.renderColor(pathSel, markerTarget, overlaySelector, renderData)
- renderElement.renderSvgMixin(pathSel, renderData)
+ renderElement.renderSvgMixinAttr(pathSel, renderData)
}
diff --git a/src/client/render/element.ts b/src/client/render/element.ts
index 1c01d4c..3d88a1c 100644
--- a/src/client/render/element.ts
+++ b/src/client/render/element.ts
@@ -23,7 +23,7 @@ export const renderSvgAttr = (selection: D3Selection, key: strin
})
}
-export const renderSvgMixin: renderFns.RenderAttrFn = (selection, renderData) => {
+export const renderSvgMixinAttr: renderFns.RenderAttrFn = (selection, renderData) => {
const precessKey = (sel, key) => [
key.includes('@') ? sel.selectAll(key.split('@')[1]) : sel,
key.includes('@') ? key.split('@')[0] : key
diff --git a/src/client/render/label/render.ts b/src/client/render/label/render.ts
index 25c3077..77ac080 100644
--- a/src/client/render/label/render.ts
+++ b/src/client/render/label/render.ts
@@ -104,5 +104,5 @@ export const render: renderFns.RenderAttrFn = (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)
}
diff --git a/src/client/render/node/render.ts b/src/client/render/node/render.ts
index f054329..a61f02a 100644
--- a/src/client/render/node/render.ts
+++ b/src/client/render/node/render.ts
@@ -77,5 +77,5 @@ export const render: renderFns.RenderAttrFn = (selection, renderDataI
renderElement.renderSvgAttr(shapeSelection, 'ry', v => v, {...cornerData, name: cornerData.name + '-y' })
}
- renderElement.renderSvgMixin(shapeSelection, renderData)
+ renderElement.renderSvgMixinAttr(shapeSelection, renderData)
}
diff --git a/src/server/CanvasSelection.ts b/src/server/CanvasSelection.ts
index 2ab46a3..112d9d6 100644
--- a/src/server/CanvasSelection.ts
+++ b/src/server/CanvasSelection.ts
@@ -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}`)
@@ -82,7 +82,12 @@ const builder: ClassBuilder> = (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))
diff --git a/src/server/EdgeSelection.ts b/src/server/EdgeSelection.ts
index 6045198..9fde7a5 100644
--- a/src/server/EdgeSelection.ts
+++ b/src/server/EdgeSelection.ts
@@ -44,7 +44,7 @@ const builder: ClassBuilder> = (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))
diff --git a/src/server/LabelSelection.ts b/src/server/LabelSelection.ts
index 46c981c..d00518e 100644
--- a/src/server/LabelSelection.ts
+++ b/src/server/LabelSelection.ts
@@ -45,7 +45,7 @@ const builder: ClassBuilder> = (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))
diff --git a/src/server/NodeSelection.ts b/src/server/NodeSelection.ts
index 7391731..22adfc1 100644
--- a/src/server/NodeSelection.ts
+++ b/src/server/NodeSelection.ts
@@ -65,7 +65,7 @@ const builder: ClassBuilder> = (contex
})
return self()
},
- ...(selection.svgMixinBuilder(context, self))
+ ...(selection.svgMixinAttrBuilder(context, self))
}, selection.builder(context, self, construct))
diff --git a/src/server/Selection.ts b/src/server/Selection.ts
index c92244e..0990c0c 100644
--- a/src/server/Selection.ts
+++ b/src/server/Selection.ts
@@ -161,7 +161,7 @@ export const builder: ClassBuilder, ISelContext>
+export const svgMixinAttrBuilder = >
(context: ISelContext, self: () => S) => ({
svgattr: (key: string, value: ElementArg) => {
diff --git a/src/server/types/canvas.ts b/src/server/types/canvas.ts
index fbdb168..ce7ce31 100644
--- a/src/server/types/canvas.ts
+++ b/src/server/types/canvas.ts
@@ -64,8 +64,8 @@ export interface CanvasSelection extends Selection {
labels (ids: ReadonlyArray): LabelSelection
/**
- * Sets the width and height of the canvas. This will only update the `width` and `height` attributes of the SVG
- * element displaying the canvas, not the enclosing HTML element.
+ * Sets the width and height of the canvas. This will determine the coordinate system, and will update the `width` and
+ * `height` attributes of the main SVG element, unless otherwise specified with [[CanvasSelection.svgattr]].
*
* @param size - A (width, height) tuple describing the size of the canvas.
*/
@@ -121,10 +121,17 @@ export interface CanvasSelection extends Selection {
*/
zoomlimit (limit: ElementArg<[NumExpr, NumExpr]>): this
+ /**
+ * Sets whether or not zooming requires the `ctrl`/`cmd` key to be held down. Disabled by default.
+ *
+ * @param required - True if the `ctrl`/`cmd` key is required, false otherwise.
+ */
+ zoomkey (required: ElementArg): this
+
/**
* Sets a custom SVG attribute on the canvas.
*
- * @param key - The name of the SVG attribute
+ * @param key - The name of the SVG attribute.
* @param value - The value of the SVG attribute.
*/
svgattr (key: string, value: ElementArg): this
diff --git a/src/server/types/edge.ts b/src/server/types/edge.ts
index ff2f28d..fb3a67f 100644
--- a/src/server/types/edge.ts
+++ b/src/server/types/edge.ts
@@ -92,7 +92,7 @@ export interface EdgeSelection extends Selection {
/**
* Sets a custom SVG attribute on the edge's path.
*
- * @param key - The name of the SVG attribute
+ * @param key - The name of the SVG attribute.
* @param value - The value of the SVG attribute.
*/
svgattr (key: string, value: ElementArg): this
diff --git a/src/server/types/label.ts b/src/server/types/label.ts
index 8bd18fd..20048f8 100644
--- a/src/server/types/label.ts
+++ b/src/server/types/label.ts
@@ -91,7 +91,7 @@ export interface LabelSelection extends Selection {
/**
* Sets a custom SVG attribute on the label's text.
*
- * @param key - The name of the SVG attribute
+ * @param key - The name of the SVG attribute.
* @param value - The value of the SVG attribute.
*/
svgattr (key: string, value: ElementArg): this
diff --git a/src/server/types/node.ts b/src/server/types/node.ts
index dfc8bbb..4d01dc5 100644
--- a/src/server/types/node.ts
+++ b/src/server/types/node.ts
@@ -115,7 +115,7 @@ export interface NodeSelection extends Selection {
/**
* Sets a custom SVG attribute on the node's shape.
*
- * @param key - The name of the SVG attribute
+ * @param key - The name of the SVG attribute.
* @param value - The value of the SVG attribute.
*/
svgattr (key: string, value: ElementArg): this
diff --git a/src/server/types/selection.ts b/src/server/types/selection.ts
index 2491161..af2e812 100644
--- a/src/server/types/selection.ts
+++ b/src/server/types/selection.ts
@@ -26,13 +26,13 @@ export interface Selection {
* @example
* ```typescript
*
- * node.color('red').size([20, 30]).svgattr('fill-opacity', 0.5)
+ * node.color('red').size([20, 30]).svgattr('stroke', 'blue')
* // is equivalent to
* node.set({
* color: 'red',
* size: [20, 30],
* svgattr: {
- * 'fill-opacity': 0.5
+ * stroke: 'blue'
* }
* })
* ```