From 3a50ad59e8505482f20fced276d4d846e995ad47 Mon Sep 17 00:00:00 2001 From: alexsocha Date: Fri, 11 Jan 2019 18:21:18 +1100 Subject: [PATCH] Added zoomkey method --- README.md | 3 +- .../attributes/definitions/animation.ts | 2 +- src/client/attributes/definitions/canvas.ts | 7 ++- src/client/client.ts | 11 +++- src/client/events.ts | 58 ++++++++++--------- src/client/render/canvas/behavior.ts | 16 ++++- src/client/render/canvas/misc.ts | 11 ++-- src/client/render/canvas/render.ts | 6 +- src/client/render/edge/render.ts | 2 +- src/client/render/element.ts | 2 +- src/client/render/label/render.ts | 2 +- src/client/render/node/render.ts | 2 +- src/server/CanvasSelection.ts | 9 ++- src/server/EdgeSelection.ts | 2 +- src/server/LabelSelection.ts | 2 +- src/server/NodeSelection.ts | 2 +- src/server/Selection.ts | 2 +- src/server/types/canvas.ts | 13 ++++- src/server/types/edge.ts | 2 +- src/server/types/label.ts | 2 +- src/server/types/node.ts | 2 +- src/server/types/selection.ts | 4 +- 22 files changed, 103 insertions(+), 59 deletions(-) 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' * } * }) * ```