Spaces:
Running
Running
| // Doc_Map_Agent Visualization Components | |
| // D3.js is required for these visualizations | |
| // This file contains the code for: | |
| // 1. Document Dependency Network Graph | |
| // 2. Process Flow Visualization | |
| // 3. Process Simulation Timeline | |
| // Sample data structures | |
| const documentTypes = [ | |
| { id: 'doc1', name: 'Clinical Study Protocol', phase: 'Clinical', owner: 'Clinical Operations' }, | |
| { id: 'doc2', name: 'Investigational Product Profile', phase: 'Preclinical', owner: 'Research' }, | |
| { id: 'doc3', name: 'Clinical Development Plan', phase: 'Clinical', owner: 'Clinical Operations' }, | |
| { id: 'doc4', name: 'Informed Consent Form', phase: 'Clinical', owner: 'Clinical Operations' }, | |
| { id: 'doc5', name: 'Case Report Form', phase: 'Clinical', owner: 'Data Management' }, | |
| { id: 'doc6', name: 'Statistical Analysis Plan', phase: 'Clinical', owner: 'Statistics' }, | |
| { id: 'doc7', name: 'Clinical Study Report', phase: 'Clinical', owner: 'Medical Writing' }, | |
| { id: 'doc8', name: 'Monitoring Plan', phase: 'Clinical', owner: 'Clinical Operations' }, | |
| { id: 'doc9', name: 'Data Management Plan', phase: 'Clinical', owner: 'Data Management' } | |
| ]; | |
| const dependencies = [ | |
| { source: 'doc2', target: 'doc1', type: 'predecessor' }, | |
| { source: 'doc3', target: 'doc1', type: 'predecessor' }, | |
| { source: 'doc1', target: 'doc4', type: 'successor' }, | |
| { source: 'doc1', target: 'doc5', type: 'successor' }, | |
| { source: 'doc1', target: 'doc6', type: 'successor' }, | |
| { source: 'doc1', target: 'doc7', type: 'successor' }, | |
| { source: 'doc1', target: 'doc8', type: 'related' }, | |
| { source: 'doc1', target: 'doc9', type: 'related' } | |
| ]; | |
| const processSteps = [ | |
| { id: 'step1', name: 'Protocol Synopsis Development', duration: 10, resources: ['Clinical Operations', 'Medical'] }, | |
| { id: 'step2', name: 'Full Protocol Drafting', duration: 15, resources: ['Medical Writing'] }, | |
| { id: 'step3', name: 'Internal Review', duration: 7, resources: ['Clinical Operations'] }, | |
| { id: 'step4', name: 'Medical/Scientific Review', duration: 7, resources: ['Medical'] }, | |
| { id: 'step5', name: 'Statistical Review', duration: 5, resources: ['Statistics'] }, | |
| { id: 'step6', name: 'Regulatory Review', duration: 10, resources: ['Regulatory'] }, | |
| { id: 'step7', name: 'Final Approval', duration: 3, resources: ['Clinical Operations'] }, | |
| { id: 'step8', name: 'Distribution', duration: 2, resources: ['Clinical Operations'] } | |
| ]; | |
| const processFlow = [ | |
| { source: 'step1', target: 'step2' }, | |
| { source: 'step2', target: 'step3' }, | |
| { source: 'step3', target: 'step4' }, | |
| { source: 'step4', target: 'step5' }, | |
| { source: 'step5', target: 'step6' }, | |
| { source: 'step6', target: 'step7' }, | |
| { source: 'step7', target: 'step8' } | |
| ]; | |
| // Initialize visualizations when the DOM is loaded | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Check if D3.js is loaded | |
| if (typeof d3 === 'undefined') { | |
| console.error('D3.js is required for visualizations'); | |
| // Add D3.js script dynamically | |
| const d3Script = document.createElement('script'); | |
| d3Script.src = 'https://d3js.org/d3.v7.min.js'; | |
| d3Script.onload = function() { | |
| initializeVisualizations(); | |
| }; | |
| document.head.appendChild(d3Script); | |
| } else { | |
| initializeVisualizations(); | |
| } | |
| }); | |
| // Initialize all visualizations | |
| function initializeVisualizations() { | |
| // Add event listeners for visualization tabs | |
| document.querySelectorAll('[data-viz-target]').forEach(tab => { | |
| tab.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| const target = this.getAttribute('data-viz-target'); | |
| showVisualization(target); | |
| }); | |
| }); | |
| // Initialize dependency network if container exists | |
| const dependencyContainer = document.getElementById('dependency-network'); | |
| if (dependencyContainer) { | |
| initDependencyNetwork(dependencyContainer); | |
| } | |
| // Initialize process flow if container exists | |
| const processFlowContainer = document.getElementById('process-flow'); | |
| if (processFlowContainer) { | |
| initProcessFlow(processFlowContainer); | |
| } | |
| // Initialize simulation timeline if container exists | |
| const simulationContainer = document.getElementById('simulation-timeline'); | |
| if (simulationContainer) { | |
| initSimulationTimeline(simulationContainer); | |
| } | |
| } | |
| // Show specific visualization and hide others | |
| function showVisualization(targetId) { | |
| document.querySelectorAll('.visualization-container').forEach(container => { | |
| container.style.display = 'none'; | |
| }); | |
| const targetContainer = document.getElementById(targetId); | |
| if (targetContainer) { | |
| targetContainer.style.display = 'block'; | |
| } | |
| // Update active tab | |
| document.querySelectorAll('[data-viz-target]').forEach(tab => { | |
| tab.classList.remove('active'); | |
| if (tab.getAttribute('data-viz-target') === targetId) { | |
| tab.classList.add('active'); | |
| } | |
| }); | |
| } | |
| // Initialize document dependency network visualization | |
| function initDependencyNetwork(container) { | |
| // Clear container | |
| container.innerHTML = ''; | |
| // Set dimensions | |
| const width = container.clientWidth; | |
| const height = 500; | |
| // Create SVG | |
| const svg = d3.select(container) | |
| .append('svg') | |
| .attr('width', width) | |
| .attr('height', height); | |
| // Create force simulation | |
| const simulation = d3.forceSimulation() | |
| .force('link', d3.forceLink().id(d => d.id).distance(100)) | |
| .force('charge', d3.forceManyBody().strength(-300)) | |
| .force('center', d3.forceCenter(width / 2, height / 2)); | |
| // Create links | |
| const link = svg.append('g') | |
| .attr('class', 'links') | |
| .selectAll('line') | |
| .data(dependencies) | |
| .enter() | |
| .append('line') | |
| .attr('stroke-width', 2) | |
| .attr('stroke', d => { | |
| if (d.type === 'predecessor') return '#3498db'; | |
| if (d.type === 'successor') return '#e74c3c'; | |
| return '#95a5a6'; | |
| }) | |
| .attr('stroke-dasharray', d => d.type === 'related' ? '5,5' : '0'); | |
| // Create nodes | |
| const node = svg.append('g') | |
| .attr('class', 'nodes') | |
| .selectAll('g') | |
| .data(documentTypes) | |
| .enter() | |
| .append('g'); | |
| // Add circles to nodes | |
| node.append('circle') | |
| .attr('r', 10) | |
| .attr('fill', d => { | |
| if (d.phase === 'Discovery') return '#3498db'; | |
| if (d.phase === 'Preclinical') return '#2ecc71'; | |
| if (d.phase === 'Clinical') return '#e74c3c'; | |
| if (d.phase === 'Regulatory') return '#f39c12'; | |
| if (d.phase === 'Medical Affairs') return '#9b59b6'; | |
| return '#95a5a6'; | |
| }); | |
| // Add text labels | |
| node.append('text') | |
| .attr('dx', 15) | |
| .attr('dy', 4) | |
| .text(d => d.name) | |
| .style('font-size', '12px'); | |
| // Add title for hover tooltip | |
| node.append('title') | |
| .text(d => `${d.name}\nPhase: ${d.phase}\nOwner: ${d.owner}`); | |
| // Update positions on simulation tick | |
| simulation.nodes(documentTypes).on('tick', () => { | |
| link | |
| .attr('x1', d => d.source.x) | |
| .attr('y1', d => d.source.y) | |
| .attr('x2', d => d.target.x) | |
| .attr('y2', d => d.target.y); | |
| node.attr('transform', d => `translate(${d.x},${d.y})`); | |
| }); | |
| // Update link source/target | |
| simulation.force('link').links(dependencies); | |
| // Add zoom functionality | |
| const zoom = d3.zoom() | |
| .scaleExtent([0.5, 3]) | |
| .on('zoom', (event) => { | |
| svg.selectAll('g').attr('transform', event.transform); | |
| }); | |
| svg.call(zoom); | |
| // Add drag functionality | |
| node.call(d3.drag() | |
| .on('start', dragstarted) | |
| .on('drag', dragged) | |
| .on('end', dragended)); | |
| function dragstarted(event, d) { | |
| if (!event.active) simulation.alphaTarget(0.3).restart(); | |
| d.fx = d.x; | |
| d.fy = d.y; | |
| } | |
| function dragged(event, d) { | |
| d.fx = event.x; | |
| d.fy = event.y; | |
| } | |
| function dragended(event, d) { | |
| if (!event.active) simulation.alphaTarget(0); | |
| d.fx = null; | |
| d.fy = null; | |
| } | |
| // Add legend | |
| const legend = svg.append('g') | |
| .attr('class', 'legend') | |
| .attr('transform', 'translate(20, 20)'); | |
| // Phase colors | |
| const phases = [ | |
| { name: 'Discovery', color: '#3498db' }, | |
| { name: 'Preclinical', color: '#2ecc71' }, | |
| { name: 'Clinical', color: '#e74c3c' }, | |
| { name: 'Regulatory', color: '#f39c12' }, | |
| { name: 'Medical Affairs', color: '#9b59b6' } | |
| ]; | |
| phases.forEach((phase, i) => { | |
| const legendRow = legend.append('g') | |
| .attr('transform', `translate(0, ${i * 20})`); | |
| legendRow.append('rect') | |
| .attr('width', 10) | |
| .attr('height', 10) | |
| .attr('fill', phase.color); | |
| legendRow.append('text') | |
| .attr('x', 15) | |
| .attr('y', 10) | |
| .text(phase.name) | |
| .style('font-size', '12px'); | |
| }); | |
| // Dependency types | |
| const depTypes = [ | |
| { name: 'Predecessor', color: '#3498db', dash: '0' }, | |
| { name: 'Successor', color: '#e74c3c', dash: '0' }, | |
| { name: 'Related', color: '#95a5a6', dash: '5,5' } | |
| ]; | |
| depTypes.forEach((type, i) => { | |
| const legendRow = legend.append('g') | |
| .attr('transform', `translate(150, ${i * 20})`); | |
| legendRow.append('line') | |
| .attr('x1', 0) | |
| .attr('y1', 5) | |
| .attr('x2', 20) | |
| .attr('y2', 5) | |
| .attr('stroke', type.color) | |
| .attr('stroke-width', 2) | |
| .attr('stroke-dasharray', type.dash); | |
| legendRow.append('text') | |
| .attr('x', 25) | |
| .attr('y', 10) | |
| .text(type.name) | |
| .style('font-size', '12px'); | |
| }); | |
| } | |
| // Initialize process flow visualization | |
| function initProcessFlow(container) { | |
| // Clear container | |
| container.innerHTML = ''; | |
| // Set dimensions | |
| const width = container.clientWidth; | |
| const height = 500; | |
| const nodeWidth = 150; | |
| const nodeHeight = 60; | |
| const nodeSpacing = 50; | |
| // Create SVG | |
| const svg = d3.select(container) | |
| .append('svg') | |
| .attr('width', width) | |
| .attr('height', height); | |
| // Create nodes | |
| const nodes = processSteps.map((step, i) => ({ | |
| ...step, | |
| x: 50 + i * (nodeWidth + nodeSpacing), | |
| y: height / 2 - nodeHeight / 2, | |
| width: nodeWidth, | |
| height: nodeHeight | |
| })); | |
| // Create links | |
| const links = processFlow.map(flow => ({ | |
| source: nodes.find(node => node.id === flow.source), | |
| target: nodes.find(node => node.id === flow.target) | |
| })); | |
| // Draw links | |
| svg.selectAll('.link') | |
| .data(links) | |
| .enter() | |
| .append('path') | |
| .attr('class', 'link') | |
| .attr('d', d => { | |
| const sourceX = d.source.x + d.source.width; | |
| const sourceY = d.source.y + d.source.height / 2; | |
| const targetX = d.target.x; | |
| const targetY = d.target.y + d.target.height / 2; | |
| return `M${sourceX},${sourceY} C${sourceX + nodeSpacing/2},${sourceY} ${targetX - nodeSpacing/2},${targetY} ${targetX},${targetY}`; | |
| }) | |
| .attr('fill', 'none') | |
| .attr('stroke', '#95a5a6') | |
| .attr('stroke-width', 2) | |
| .attr('marker-end', 'url(#arrowhead)'); | |
| // Add arrowhead marker | |
| svg.append('defs').append('marker') | |
| .attr('id', 'arrowhead') | |
| .attr('viewBox', '0 -5 10 10') | |
| .attr('refX', 8) | |
| .attr('refY', 0) | |
| .attr('markerWidth', 6) | |
| .attr('markerHeight', 6) | |
| .attr('orient', 'auto') | |
| .append('path') | |
| .attr('d', 'M0,-5L10,0L0,5') | |
| .attr('fill', '#95a5a6'); | |
| // Draw nodes | |
| const nodeGroups = svg.selectAll('.node') | |
| .data(nodes) | |
| .enter() | |
| .append('g') | |
| .attr('class', 'node') | |
| .attr('transform', d => `translate(${d.x}, ${d.y})`); | |
| nodeGroups.append('rect') | |
| .attr('width', d => d.width) | |
| .attr('height', d => d.height) | |
| .attr('rx', 5) | |
| .attr('ry', 5) | |
| .attr('fill', '#3498db') | |
| .attr('stroke', '#2980b9') | |
| .attr('stroke-width', 1); | |
| nodeGroups.append('text') | |
| .attr('x', d => d.width / 2) | |
| .attr('y', d => d.height / 2 - 10) | |
| .attr('text-anchor', 'middle') | |
| .attr('fill', 'white') | |
| .style('font-size', '12px') | |
| .style('font-weight', 'bold') | |
| .text(d => d.name); | |
| nodeGroups.append('text') | |
| .attr('x', d => d.width / 2) | |
| .attr('y', d => d.height / 2 + 10) | |
| .attr('text-anchor', 'middle') | |
| .attr('fill', 'white') | |
| .style('font-size', '10px') | |
| .text(d => `${d.duration} days`); | |
| // Add zoom functionality | |
| const zoom = d3.zoom() | |
| .scaleExtent([0.5, 2]) | |
| .on('zoom', (event) => { | |
| svg.selectAll('g').attr('transform', event.transform); | |
| }); | |
| svg.call(zoom); | |
| } | |
| // Initialize simulation timeline visualization | |
| function initSimulationTimeline(container) { | |
| // Clear container | |
| container.innerHTML = ''; | |
| // Set dimensions | |
| const width = container.clientWidth; | |
| const height = 500; | |
| const margin = { top: 50, right: 50, bottom: 50, left: 150 }; | |
| const innerWidth = width - margin.left - margin.right; | |
| const innerHeight = height - margin.top - margin.bottom; | |
| // Create SVG | |
| const svg = d3.select(container) | |
| .append('svg') | |
| .attr('width', width) | |
| .attr('height', height); | |
| // Create main group | |
| const g = svg.append('g') | |
| .attr('transform', `translate(${margin.left}, ${margin.top})`); | |
| // Create simulation data | |
| const simulationData = processSteps.map((step, i) => { | |
| // Calculate start and end dates | |
| let startDate = new Date('2025-04-15'); | |
| if (i > 0) { | |
| const prevStep = processSteps[i - 1]; | |
| startDate = new Date('2025-04-15'); | |
| startDate.setDate(startDate.getDate() + prevStep.duration); | |
| } | |
| const endDate = new Date(startDate); | |
| endDate.setDate(endDate.getDate() + step.duration); | |
| return { | |
| ...step, | |
| startDate, | |
| endDate | |
| }; | |
| }); | |
| // Create scales | |
| const yScale = d3.scaleBand() | |
| .domain(simulationData.map(d => d.name)) | |
| .range([0, innerHeight]) | |
| .padding(0.2); | |
| const xScale = d3.scaleTime() | |
| .domain([ | |
| new Date('2025-04-15'), | |
| new Date('2025-10-15') | |
| ]) | |
| .range([0, innerWidth]); | |
| // Create axes | |
| const xAxis = d3.axisBottom(xScale) | |
| .ticks(d3.timeMonth.every(1)) | |
| .tickFormat(d3.timeFormat('%b %Y')); | |
| const yAxis = d3.axisLeft(yScale); | |
| g.append('g') | |
| .attr('class', 'x-axis') | |
| .attr('transform', `translate(0, ${innerHeight})`) | |
| .call(xAxis); | |
| g.append('g') | |
| .attr('class', 'y-axis') | |
| .call(yAxis); | |
| // Create bars | |
| g.selectAll('.bar') | |
| .data(simulationData) | |
| .enter() | |
| .append('rect') | |
| .attr('class', 'bar') | |
| .attr('x', d => xScale(d.startDate)) | |
| .attr('y', d => yScale(d.name)) | |
| .attr('width', d => xScale(d.endDate) - xScale(d.startDate)) | |
| .attr('height', yScale.bandwidth()) | |
| .attr('fill', (d, i) => { | |
| // Highlight bottlenecks | |
| if (i === 2 || i === 4 || i === 5) { | |
| return '#e74c3c'; | |
| } | |
| return '#3498db'; | |
| }) | |
| .attr('stroke', '#2c3e50') | |
| .attr('stroke-width', 1); | |
| // Add text labels | |
| g.selectAll('.bar-label') | |
| .data(simulationData) | |
| .enter() | |
| .append('text') | |
| .attr('class', 'bar-label') | |
| .attr('x', d => xScale(d.startDate) + (xScale(d.endDate) - xScale(d.startDate)) / 2) | |
| .attr('y', d => yScale(d.name) + yScale.bandwidth() / 2) | |
| .attr('text-anchor', 'middle') | |
| .attr('dominant-baseline', 'middle') | |
| .attr('fill', 'white') | |
| .style('font-size', '10px') | |
| .text(d => `${d.duration} days`); | |
| // Add today marker | |
| const today = new Date('2025-06-15'); | |
| g.append('line') | |
| .attr('x1', xScale(today)) | |
| .attr('y1', 0) | |
| .attr('x2', xScale(today)) | |
| .attr('y2', innerHeight) | |
| .attr('stroke', '#2c3e50') | |
| .attr('stroke-width', 2) | |
| .attr('stroke-dasharray', '5,5'); | |
| g.append('text') | |
| .attr('x', xScale(today)) | |
| .attr('y', -10) | |
| .attr('text-anchor', 'middle') | |
| .text('Today') | |
| .style('font-size', '12px') | |
| .style('font-weight', 'bold'); | |
| // Add target end date marker | |
| const targetEndDate = new Date('2025-10-15'); | |
| g.append('line') | |
| .attr('x1', xScale(targetEndDate)) | |
| .attr('y1', 0) | |
| .attr('x2', xScale(targetEndDate)) | |
| .attr('y2', innerHeight) | |
| .attr('stroke', '#e74c3c') | |
| .attr('stroke-width', 2) | |
| .attr('stroke-dasharray', '5,5'); | |
| g.append('text') | |
| .attr('x', xScale(targetEndDate)) | |
| .attr('y', -10) | |
| .attr('text-anchor', 'middle') | |
| .attr('fill', '#e74c3c') | |
| .text('Target End Date') | |
| .style('font-size', '12px') | |
| .style('font-weight', 'bold'); | |
| // Add legend | |
| const legend = svg.append('g') | |
| .attr('class', 'legend') | |
| .attr('transform', `translate(${margin.left}, ${height - 20})`); | |
| legend.append('rect') | |
| .attr('width', 15) | |
| .attr('height', 15) | |
| .attr('fill', '#3498db'); | |
| legend.append('text') | |
| .attr('x', 20) | |
| .attr('y', 12) | |
| .text('Normal Process') | |
| .style('font-size', '12px'); | |
| legend.append('rect') | |
| .attr('width', 15) | |
| .attr('height', 15) | |
| .attr('fill', '#e74c3c') | |
| .attr('transform', 'translate(150, 0)'); | |
| legend.append('text') | |
| .attr('x', 170) | |
| .attr('y', 12) | |
| .text('Bottleneck') | |
| .style('font-size', '12px'); | |
| } | |