UNPKG

squarified

Version:
671 lines (662 loc) 26 kB
import { h as definePlugin, B as smoothFrame, r as createRoundBlock, z as stackMatrixTransform, S as Schedule, H as DEFAULT_MATRIX_LOC, F as isScrollWheelOrRightButtonOnMouseupAndDown, A as stackMatrixTransformWithGraphAndLayer, j as isContextMenuEvent, o as hashCode, P as PI_2, n as isWheelEvent, m as mixin } from './dom-event-DrYYfglv.mjs'; // Currently, etoile is an internal module, so we won't need too much easing functions. // And the animation logic is implemented by user code. const easing = { linear: (k)=>k, quadraticIn: (k)=>k * k, quadraticOut: (k)=>k * (2 - k), quadraticInOut: (k)=>{ if ((k *= 2) < 1) { return 0.5 * k * k; } return -0.5 * (--k * (k - 2) - 1); }, cubicIn: (k)=>k * k * k, cubicOut: (k)=>{ if ((k *= 2) < 1) { return 0.5 * k * k * k; } return 0.5 * ((k -= 2) * k * k + 2); }, cubicInOut: (k)=>{ if ((k *= 2) < 1) { return 0.5 * k * k * k; } return 0.5 * ((k -= 2) * k * k + 2); } }; class Highlight extends Schedule { reset() { this.destory(); this.update(); } get canvas() { return this.render.canvas; } setZIndexForHighlight(zIndex = '-1') { this.canvas.style.zIndex = zIndex; } init() { this.setZIndexForHighlight(); this.canvas.style.position = 'absolute'; this.canvas.style.pointerEvents = 'none'; } } const ANIMATION_DURATION = 300; const HIGH_LIGHT_OPACITY = 0.3; const fill = { desc: { r: 255, g: 255, b: 255 }, mode: 'rgb' }; const presetHighlightPlugin = definePlugin({ name: 'treemap:preset-highlight', onLoad () { const meta = this.getPluginMetadata('treemap:preset-highlight'); if (!meta) { return; } if (!meta.highlight) { meta.highlight = new Highlight(this.instance.to); } }, onDOMEventTriggered (name, _, module, { stateManager: state, matrix }) { if (name === 'mousemove') { if (state.canTransition('MOVE')) { const meta = this.getPluginMetadata('treemap:preset-highlight'); if (!module) { meta.highlight?.reset(); meta.highlight?.update(); meta.highlight?.setZIndexForHighlight(); meta.highlightSeq = (meta.highlightSeq ?? 0) + 1; return; } const [x, y, w, h] = module.layout; const effectiveRadius = Math.min(module.config.rectRadius, w / 4, h / 4); meta.highlightSeq = (meta.highlightSeq ?? 0) + 1; const thisSeq = meta.highlightSeq; smoothFrame((_, cleanup)=>{ if (meta.highlightSeq !== thisSeq) { meta.highlight?.setZIndexForHighlight(); cleanup(); return; } cleanup(); meta.highlight?.reset(); const mask = createRoundBlock(x, y, w, h, { fill, opacity: HIGH_LIGHT_OPACITY, radius: effectiveRadius, padding: 0 }); meta.highlight?.add(mask); meta.highlight?.setZIndexForHighlight('1'); stackMatrixTransform(mask, matrix.e, matrix.f, 1); meta.highlight?.update(); }, { duration: ANIMATION_DURATION }); } } }, onResize () { const meta = this.getPluginMetadata('treemap:preset-highlight'); if (!meta) { return; } meta.highlight?.render.initOptions({ ...this.instance.render.options }); meta.highlight?.reset(); meta.highlight?.init(); }, onDispose () { const meta = this.getPluginMetadata('treemap:preset-highlight'); if (meta && meta.highlight) { meta.highlight.destory(); meta.highlight = null; } }, meta: { highlight: null, highlightSeq: 0 } }); const presetDragElementPlugin = definePlugin({ name: 'treemap:preset-drag-element', onDOMEventTriggered (name, event, module, domEvent) { const { stateManager: state, matrix, component } = domEvent; switch(name){ case 'mousemove': { if (state.isInState('DRAGGING')) { domEvent.silent('click'); } else { domEvent.active('click'); } const meta = getDragOptions.call(this); if (!meta) { return; } if (meta.dragOptions.x === 0 && meta.dragOptions.y === 0) { state.transition('IDLE'); return; } state.transition('DRAGGING'); if (state.isInState('DRAGGING')) { const highlight = getHighlightInstance.call(this); smoothFrame((_, cleanup)=>{ cleanup(); const { offsetX, offsetY } = event.native; const drawX = offsetX - meta.dragOptions.x; const drawY = offsetY - meta.dragOptions.y; const lastX = meta.dragOptions.x; const lastY = meta.dragOptions.y; if (highlight?.highlight) { highlight.highlight.reset(); highlight.highlight.setZIndexForHighlight(); } matrix.translation(drawX, drawY); meta.dragOptions.x = offsetX; meta.dragOptions.y = offsetY; meta.dragOptions.lastX = lastX; meta.dragOptions.lastY = lastY; component.cleanup(); component.draw(false, false); stackMatrixTransformWithGraphAndLayer(component.elements, matrix.e, matrix.f, 1); component.update(); return true; }, { duration: ANIMATION_DURATION, deps: [ ()=>state.isInState('IDLE') ] }); } break; } case 'mouseup': { if (state.isInState('PRESSED')) { const meta = getDragOptions.call(this); if (meta && meta.dragOptions) { if (meta.dragOptions.x === meta.dragOptions.lastX && meta.dragOptions.y === meta.dragOptions.lastY) { state.transition('IDLE'); return; } } } if (state.isInState('DRAGGING') && state.canTransition('IDLE')) { const highlight = getHighlightInstance.call(this); if (highlight && highlight.highlight) { highlight.highlight.reset(); highlight.highlight.setZIndexForHighlight(); } const meta = getDragOptions.call(this); if (meta && meta.dragOptions) { meta.dragOptions.x = 0; meta.dragOptions.y = 0; meta.dragOptions.lastX = 0; meta.dragOptions.lastY = 0; state.transition('IDLE'); } } break; } case 'mousedown': { if (isScrollWheelOrRightButtonOnMouseupAndDown(event.native)) { return; } const meta = getDragOptions.call(this); if (!meta) { return; } meta.dragOptions.x = event.native.offsetX; meta.dragOptions.y = event.native.offsetY; meta.dragOptions.lastX = event.native.offsetX; meta.dragOptions.lastY = event.native.offsetY; state.transition('PRESSED'); break; } } }, meta: { dragOptions: { x: 0, y: 0, lastX: 0, lastY: 0 } }, onResize ({ matrix, stateManager: state }) { matrix.create(DEFAULT_MATRIX_LOC); state.reset(); } }); function getHighlightInstance() { return this.getPluginMetadata('treemap:preset-highlight'); } function getDragOptions() { const meta = this.getPluginMetadata('treemap:preset-drag-element'); return meta; } function presetMenuPlugin(options) { let menu = null; let domEvent = null; const handleMenuClick = (e)=>{ if (!domEvent) { return; } if (!menu) { return; } const target = e.target; if (target.parentNode) { const parent = target.parentNode; const action = parent.getAttribute('data-action'); if (!action) { return; } if (options?.onClick) { options.onClick(action, domEvent.findRelativeNode({ native: e, kind: undefined })); } } menu.style.display = 'none'; }; return definePlugin({ name: 'treemap:preset-menu', onDOMEventTriggered (_, event, __, DOMEvent) { if (isContextMenuEvent(event)) { event.native.stopPropagation(); event.native.preventDefault(); if (!menu) { menu = document.createElement('div'); domEvent = DOMEvent; Object.assign(menu.style, { backgroundColor: '#fff', ...options?.style, position: 'absolute', zIndex: '9999' }); menu.addEventListener('click', handleMenuClick); if (menu && options?.render) { const result = options.render(menu); menu.innerHTML = result.map((item)=>{ return `<div data-action='${item.action}'>${item.html}</div>`; }).join(''); } document.body.append(menu); } menu.style.left = event.native.clientX + 'px'; menu.style.top = event.native.clientY + 'px'; menu.style.display = 'initial'; } }, onDispose () { if (!menu) { return; } menu.removeEventListener('click', handleMenuClick); menu = null; domEvent = null; } }); } const presetColorPlugin = definePlugin({ name: 'treemap:preset-color', onModuleInit (modules) { const colorMappings = {}; for(let i = 0; i < modules.length; i++){ const module = modules[i]; assignColorMappings(colorMappings, module, Math.abs(hashCode(module.node.id)) % PI_2, 0); } return { colorMappings }; } }); function assignColorMappings(colorMappings, module, ancestorHue, depth) { const hueOffset = Math.abs(hashCode(module.node.id)) % 60 - 30; const hue = (ancestorHue + hueOffset) % 360; const saturation = Math.max(75 - depth * 5, 40); const baseLightness = 55 - depth * 3; const color = adjustColorToComfortableForHumanEye(hue, saturation, baseLightness); colorMappings[module.node.id] = color; if (module.node.isCombinedNode && module.node.originalNodes) { for (const combined of module.node.originalNodes){ colorMappings[combined.id] = color; } } if (module.children && module.children.length) { const childCount = module.children.length; for(let i = 0; i < childCount; i++){ const childHueOffset = 40 * i / childCount; const childHue = (hue + childHueOffset) % 360; assignColorMappings(colorMappings, module.children[i], childHue, depth + 1); } } } function adjustColorToComfortableForHumanEye(hue, saturation, lightness) { hue = (hue % 360 + 360) % 360; saturation = Math.min(Math.max(saturation, 40), 85); lightness = Math.min(Math.max(lightness, 35), 75); if (hue >= 60 && hue <= 180) { saturation = Math.max(saturation - 10, 40); lightness = Math.min(lightness + 5, 75); } else if (hue >= 200 && hue <= 280) { lightness = Math.min(lightness + 8, 75); } else if (hue >= 0 && hue <= 30) { saturation = Math.max(saturation - 5, 40); } return { mode: 'hsl', desc: { h: hue, s: saturation, l: lightness } }; } function presetScalePlugin(options) { return definePlugin({ name: 'treemap:preset-scale', onDOMEventTriggered (_, event, module, evt) { if (isWheelEvent(event)) { onWheel(this, event, evt); } }, meta: { scaleOptions: { scale: 1, minScale: options?.min || 0.1, maxScale: options?.max || Infinity, scaleFactor: 0.05 }, gestureState: { isTrackingGesture: false, lastEventTime: 0, eventCount: 0, totalDeltaY: 0, totalDeltaX: 0, consecutivePinchEvents: 0, gestureType: 'unknown', lockGestureType: false } }, onResize ({ matrix, stateManager: state }) { const meta = getScaleOptions.call(this); if (meta) { meta.scaleOptions.scale = 1; } matrix.create(DEFAULT_MATRIX_LOC); state.reset(); } }); } function getScaleOptions() { const meta = this.getPluginMetadata('treemap:preset-scale'); return meta; } function determineGestureType(event, gestureState) { const now = Date.now(); const timeDiff = now - gestureState.lastEventTime; if (timeDiff > 150) { Object.assign(gestureState, { isTrackingGesture: false, lastEventTime: now, eventCount: 1, totalDeltaY: Math.abs(event.deltaY), totalDeltaX: Math.abs(event.deltaX), consecutivePinchEvents: 0, gestureType: 'unknown', lockGestureType: false }); } else { gestureState.eventCount++; gestureState.totalDeltaY += Math.abs(event.deltaY); gestureState.totalDeltaX += Math.abs(event.deltaX); gestureState.lastEventTime = now; } if (event.ctrlKey) { gestureState.gestureType = 'zoom'; gestureState.lockGestureType = true; return 'zoom'; } // windows/macos mouse wheel // Usually the dettaY is large and deltaX maybe 0 or small number. const isMouseWheel = Math.abs(event.deltaX) >= 100 && Math.abs(event.deltaX) <= 10 || Math.abs(event.deltaY) > 50 && Math.abs(event.deltaX) < Math.abs(event.deltaY) * 0.1; if (isMouseWheel) { gestureState.gestureType = 'zoom'; gestureState.lockGestureType = true; return 'zoom'; } if (gestureState.lockGestureType && gestureState.gestureType !== 'unknown') { return gestureState.gestureType; } // Magic Trackpad or Precision Touchpad if (gestureState.eventCount >= 3) { const avgDeltaY = gestureState.totalDeltaY / gestureState.eventCount; const avgDeltaX = gestureState.totalDeltaX / gestureState.eventCount; const ratio = avgDeltaX / (avgDeltaY + 0.1); const isZoomGesture = avgDeltaY > 8 && ratio < 0.3 && Math.abs(event.deltaY) > 5; if (isZoomGesture) { gestureState.gestureType = 'zoom'; gestureState.lockGestureType = true; return 'zoom'; } else { gestureState.gestureType = 'pan'; gestureState.lockGestureType = true; return 'pan'; } } return 'pan'; } function onWheel(pluginContext, event, domEvent) { event.native.preventDefault(); const meta = getScaleOptions.call(pluginContext); if (!meta) { return; } const gestureType = determineGestureType(event.native, meta.gestureState); if (gestureType === 'zoom') { handleZoom(pluginContext, event, domEvent); } else { handlePan(pluginContext, event, domEvent); } } function updateViewport(pluginContext, { stateManager: state, component, matrix }, useAnimation = false) { const highlight = getHighlightInstance.apply(pluginContext); const doUpdate = ()=>{ if (highlight && highlight.highlight) { highlight.highlight.reset(); highlight.highlight.setZIndexForHighlight(); } if (highlight) { highlight.highlightSeq = (highlight.highlightSeq ?? 0) + 1; } component.cleanup(); const { width, height } = component.render.options; component.layoutNodes = component.calculateLayoutNodes(component.data, { w: width * matrix.a, h: height * matrix.d, x: 0, y: 0 }, 1); component.draw(true, false); stackMatrixTransformWithGraphAndLayer(component.elements, matrix.e, matrix.f, 1); component.update(); if (state.canTransition('IDLE')) { state.transition('IDLE'); } }; if (useAnimation) { smoothFrame((_, cleanup)=>{ cleanup(); doUpdate(); return true; }, { duration: ANIMATION_DURATION }); } else { doUpdate(); } } function handleZoom(pluginContext, event, domEvent) { const { stateManager: state, matrix, component } = domEvent; const meta = getScaleOptions.call(pluginContext); if (!meta) { return; } const { scale, minScale, maxScale, scaleFactor } = meta.scaleOptions; const oldMatrix = { e: matrix.e, f: matrix.f }; const dynamicScaleFactor = Math.max(scaleFactor, scale * 0.1); const delta = event.native.deltaY < 0 ? dynamicScaleFactor : -dynamicScaleFactor; const newScale = Math.max(minScale, Math.min(maxScale, scale + delta)); if (newScale === scale) { return; } state.transition('SCALING'); const mouseX = event.native.offsetX; const mouseY = event.native.offsetY; const scaleDiff = newScale / scale; meta.scaleOptions.scale = newScale; matrix.scale(scaleDiff, scaleDiff); matrix.e = mouseX - (mouseX - matrix.e) * scaleDiff; matrix.f = mouseY - (mouseY - matrix.f) * scaleDiff; const newMatrix = { e: matrix.e, f: matrix.f }; component.handleTransformCacheInvalidation(oldMatrix, newMatrix); updateViewport(pluginContext, domEvent, false); } function handlePan(pluginContext, event, domEvent) { const { stateManager: state, matrix } = domEvent; const panSpeed = 0.8; const deltaX = event.native.deltaX * panSpeed; const deltaY = event.native.deltaY * panSpeed; state.transition('PANNING'); matrix.e -= deltaX; matrix.f -= deltaY; updateViewport(pluginContext, domEvent, true); } const MAX_SCALE_MULTIPLIER = 2.0; const ZOOM_PADDING_RATIO = 0.85; const presetZoomablePlugin = definePlugin({ name: 'treemap:preset-zoomable', onLoad (treemap, { stateManager: state, matrix }) { return mixin(treemap, [ { name: 'zoom', fn: ()=>(id)=>{ const meta = this.getPluginMetadata('treemap:preset-zoomable'); if (!meta || state.isInState('ZOOMING')) { return; } const targetModule = this.resolveModuleById(id); if (!targetModule) { return; } const oldMatrix = { e: matrix.e, f: matrix.f, a: matrix.a }; meta.previousMatrixState = { e: matrix.e, f: matrix.f, a: matrix.a, d: matrix.d }; const component = this.instance; state.transition('ZOOMING'); const [nodeX, nodeY, nodeW, nodeH] = targetModule.layout; const { width, height } = component.render.options; const currentScale = matrix.a; // To prevent unlimited scale factor growth. const scaleX = width * ZOOM_PADDING_RATIO / nodeW; const scaleY = height * ZOOM_PADDING_RATIO / nodeH; const idleScale = Math.min(scaleX, scaleY); const maxAllowedScale = currentScale * MAX_SCALE_MULTIPLIER; const targetScale = Math.max(currentScale, Math.min(idleScale, maxAllowedScale)); // Real world args const viewportCenterX = width / 2; const viewportCenterY = height / 2; const originalNodeCenterX = (nodeX + nodeW / 2) / currentScale; const originalNodeCenterY = (nodeY + nodeH / 2) / currentScale; const targetE = viewportCenterX - originalNodeCenterX * targetScale; const targetF = viewportCenterY - originalNodeCenterY * targetScale; const scaleMeta = getScaleOptions.call(this); if (scaleMeta) { scaleMeta.scaleOptions.scale = targetScale; } const highlight = getHighlightInstance.call(this); const dragMeta = getDragOptions.call(this); if (dragMeta) { Object.assign(dragMeta.dragOptions, { x: 0, y: 0, lastX: 0, lastY: 0 }); } const startMatrix = { e: matrix.e, f: matrix.f, a: matrix.a, d: matrix.d }; const finalMatrix = { e: targetE, f: targetF }; component.handleTransformCacheInvalidation(oldMatrix, finalMatrix); smoothFrame((progress)=>{ const easedProgress = easing.cubicInOut(progress); matrix.create(DEFAULT_MATRIX_LOC); matrix.e = startMatrix.e + (targetE - startMatrix.e) * easedProgress; matrix.f = startMatrix.f + (targetF - startMatrix.f) * easedProgress; matrix.a = startMatrix.a + (targetScale - startMatrix.a) * easedProgress; matrix.d = startMatrix.d + (targetScale - startMatrix.d) * easedProgress; if (highlight?.highlight) { highlight.highlight.reset(); highlight.highlight.setZIndexForHighlight(); } if (highlight) { highlight.highlightSeq = (highlight.highlightSeq ?? 0) + 1; } component.cleanup(); component.layoutNodes = component.calculateLayoutNodes(component.data, { w: width * matrix.a, h: height * matrix.d, x: 0, y: 0 }, 1); component.draw(true, false); stackMatrixTransformWithGraphAndLayer(component.elements, matrix.e, matrix.f, 1); component.update(); }, { duration: ANIMATION_DURATION, onStop: ()=>{ state.reset(); } }); } } ]); }, meta: { isZooming: false } }); export { presetColorPlugin, presetDragElementPlugin, presetHighlightPlugin, presetMenuPlugin, presetScalePlugin, presetZoomablePlugin };