var selectedPdServiceId; // 제품(서비스) 아이디 var selectedVersionId; // 선택된 버전 아이디 var dataTableRef; // 최상단 메뉴 변수 var req_state, resource_info, issue_info, period_info, total_days_progress; var pdServiceData; var pdServiceListData; var versionListData; //////////////////////////////////////////////////////////////////////////////////////// //Document Ready //////////////////////////////////////////////////////////////////////////////////////// function execDocReady() { var pluginGroups = [ [ "../reference/light-blue/lib/vendor/jquery.ui.widget.js", "../reference/light-blue/lib/vendor/http_blueimp.github.io_JavaScript-Templates_js_tmpl.js", "../reference/light-blue/lib/vendor/http_blueimp.github.io_JavaScript-Load-Image_js_load-image.js", "../reference/light-blue/lib/vendor/http_blueimp.github.io_JavaScript-Canvas-to-Blob_js_canvas-to-blob.js", "../reference/light-blue/lib/jquery.iframe-transport.js", "../reference/lightblue4/docs/lib/widgster/widgster.js", "../reference/lightblue4/docs/lib/slimScroll/jquery.slimscroll.min.js" ], [ "../reference/jquery-plugins/select2-4.0.2/dist/css/select2_lightblue4.css", "../reference/jquery-plugins/lou-multi-select-0.9.12/css/multiselect-lightblue4.css", "../reference/jquery-plugins/multiple-select-1.5.2/dist/multiple-select-bluelight.css", "../reference/jquery-plugins/select2-4.0.2/dist/js/select2.min.js", "../reference/jquery-plugins/lou-multi-select-0.9.12/js/jquery.quicksearch.js", "../reference/jquery-plugins/lou-multi-select-0.9.12/js/jquery.multi-select.js", "../reference/jquery-plugins/multiple-select-1.5.2/dist/multiple-select.min.js" ], [ "../reference/jquery-plugins/dataTables-1.10.16/media/css/jquery.dataTables_lightblue4.css", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Responsive/css/responsive.dataTables_lightblue4.css", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Select/css/select.dataTables_lightblue4.css", "../reference/jquery-plugins/dataTables-1.10.16/media/js/jquery.dataTables.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Responsive/js/dataTables.responsive.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Select/js/dataTables.select.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/RowGroup/js/dataTables.rowsGroup.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/dataTables.buttons.min.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/buttons.html5.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/buttons.print.js", "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/jszip.min.js", ], [ // d3-5.16.0 네트워크 차트 "../reference/jquery-plugins/d3-5.16.0/d3.min.js", // 최상단 메뉴 "js/analysis/topmenu/topMenuApi.js", "./js/common/chart/eCharts/basicRadar.js", // 버전 별 요구사항 현황 (RadialPolarBarChart) "./js/common/chart/eCharts/RadialPolarBarChart.js", //CirclePacking with d3 Chart "./js/common/chart/eCharts/circularPackingChart.js", // 투입 인력별 요구사항 관여 차트 "../reference/jquery-plugins/Jit-2.0.1/jit.js", "../reference/jquery-plugins/Jit-2.0.1/Examples/css/Treemap.css", // 제품-버전-투입인력 차트 "../reference/jquery-plugins/d3-sankey-v0.12.3/d3-sankey.min.js", //chart Colors "./js/common/colorPalette.js", // Apache Echarts "../reference/jquery-plugins/echarts-5.4.3/dist/echarts.min.js", "./js/common/chart/d3/sankey.js" ] ]; loadPluginGroupsParallelAndSequential(pluginGroups) .then(function () { //vfs_fonts 파일이 커서 defer 처리 함. setTimeout(function () { var script = document.createElement("script"); script.src = "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/vfs_fonts.js"; script.defer = true; // defer 속성 설정 document.head.appendChild(script); }, 5000); // 5초 후에 실행됩니다. //pdfmake 파일이 커서 defer 처리 함. setTimeout(function () { var script = document.createElement("script"); script.src = "../reference/jquery-plugins/dataTables-1.10.16/extensions/Buttons/js/pdfmake.min.js"; script.defer = true; // defer 속성 설정 document.head.appendChild(script); }, 5000); // 5초 후에 실행됩니다. // 사이드 메뉴 색상 설정 $(".widget").widgster(); setSideMenu("sidebar_menu_analysis", "sidebar_menu_analysis_scope"); //제품(서비스) 셀렉트 박스 이니시에이터 makePdServiceSelectBox(); //버전 멀티 셀렉트 박스 이니시에이터 makeVersionMultiSelectBox(); // 높이 조정 $('.top-menu-div').matchHeight({ target: $('.top-menu-div-scope') }); }) .catch(function () { console.error("플러그인 로드 중 오류 발생"); }); } /////////////////////// //제품 서비스 셀렉트 박스 ////////////////////// function makePdServiceSelectBox() { //제품 서비스 셀렉트 박스 이니시에이터 $(".chzn-select").each(function () { $(this).select2($(this).data()); }); //제품 서비스 셀렉트 박스 데이터 바인딩 $.ajax({ url: "/auth-user/api/arms/pdServicePure/getPdServiceMonitor.do", type: "GET", contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, statusCode: { 200: function (data) { ////////////////////////////////////////////////////////// pdServiceListData = []; for (var k in data.response) { var obj = data.response[k]; pdServiceListData.push({ "pdServiceId": obj.c_id, "pdServiceName": obj.c_title }); var newOption = new Option(obj.c_title, obj.c_id, false, false); $("#selected_pdService").append(newOption).trigger("change"); } ////////////////////////////////////////////////////////// console.log("[analysisScope :: makePdServiceSelectBox] :: pdServiceListData => " ); console.table(pdServiceListData); } } }); $("#selected_pdService").on("select2:open", function () { //슬림스크롤 makeSlimScroll(".select2-results__options"); }); // --- select2 ( 제품(서비스) 검색 및 선택 ) 이벤트 --- // $("#selected_pdService").on("select2:select", function (e) { selectedPdServiceId = $("#selected_pdService").val(); //refreshDetailChart(); 변수값_초기화(); // 제품( 서비스 ) 선택했으니까 자동으로 버전을 선택할 수 있게 유도 // 디폴트는 base version 을 선택하게 하고 ( select all ) //~> 이벤트 연계 함수 :: Version 표시 jsTree 빌드 bind_VersionData_By_PdService(); }); } // end makePdServiceSelectBox() //////////////////////////////////////// //버전 멀티 셀렉트 박스 //////////////////////////////////////// function makeVersionMultiSelectBox() { //버전 선택시 셀렉트 박스 이니시에이터 $(".multiple-select").multipleSelect({ filter: true, onClose: function () { console.log("onOpen event fire!\n"); var checked = $("#checkbox1").is(":checked"); var endPointUrl = ""; var versionTag = $(".multiple-select").val(); console.log("[ analysisScope :: makeVersionMultiSelectBox ] :: versionTag"); console.log(versionTag); selectedVersionId = versionTag.join(","); if (versionTag === null || versionTag == "") { alert("버전이 선택되지 않았습니다."); $(".ms-parent").css("z-index", 1000); return; } // 최상단 메뉴 세팅 TopMenuApi.톱메뉴_초기화(); TopMenuApi.톱메뉴_세팅(); // Circular Packing with D3 차트 getReqStatusAndInvolvedAssignees(selectedPdServiceId, selectedVersionId); // 네트워크 차트 // network_chart_load($("#selected_pdService").val(), selectedVersionId); statisticsMonitor($("#selected_pdService").val(), selectedVersionId); getRelationJiraIssueByPdServiceAndVersions($("#selected_pdService").val(), selectedVersionId); // 나이팅게일로즈 차트(pie) - 버전별 요구사항 getReqPerMappedVersions(selectedPdServiceId, selectedVersionId); treeBar(); var 요구사항_호출_주소 = "/state-per-version/T_ARMS_REQADD_" + selectedPdServiceId + "/getReqAddListByFilter?" + "pdServiceAndIsReq.pdServiceLink="+selectedPdServiceId+"&pdServiceAndIsReq.pdServiceVersionLinks="+selectedVersionId; 요구사항_현황_데이터_테이블($("#selected_pdService").val(), 요구사항_호출_주소); $(".ms-parent").css("z-index", 1000); }, onOpen: function() { console.log("open event"); $(".ms-parent").css("z-index", 9999); } }); } function bind_VersionData_By_PdService() { $(".multiple-select option").remove(); $.ajax({ url: "/auth-user/api/arms/pdService/getVersionList?c_id=" + $("#selected_pdService").val(), type: "GET", dataType: "json", progress: true, statusCode: { 200: function (data) { ////////////////////////////////////////////////////////// var pdServiceVersionIds = []; versionListData = []; for (var k in data.response) { var obj = data.response[k]; pdServiceVersionIds.push(obj.c_id); versionListData.push(obj); var newOption = new Option(obj.c_title, obj.c_id, true, false); $(".multiple-select").append(newOption); } var versionTag = $(".multiple-select").val(); console.log("[ analysisScope :: bind_VersionData_By_PdService ] :: versionTag"); console.log(pdServiceVersionIds); selectedVersionId = pdServiceVersionIds.join(","); console.log("bind_VersionData_By_PdService :: selectedVersionId"); console.log(selectedVersionId); // 최상단 메뉴 세팅 TopMenuApi.톱메뉴_초기화(); TopMenuApi.톱메뉴_세팅(); // Circular Packing with D3 차트 getReqStatusAndInvolvedAssignees(selectedPdServiceId, selectedVersionId); // 네트워크 차트 // network_chart_load($("#selected_pdService").val(), selectedVersionId); statisticsMonitor($("#selected_pdService").val(), selectedVersionId); getRelationJiraIssueByPdServiceAndVersions($("#selected_pdService").val(), selectedVersionId); // 나이팅게일로즈 차트(pie) - 버전별 요구사항 getReqPerMappedVersions(selectedPdServiceId, selectedVersionId); if (data.length > 0) { console.log("display 재설정."); } treeBar(); $(".multiple-select").multipleSelect("refresh"); //요구사항 현황 데이터 테이블 로드 var 요구사항_호출_주소 = "/state-per-version/T_ARMS_REQADD_" + selectedPdServiceId + "/getReqAddListByFilter?" + "pdServiceAndIsReq.pdServiceId="+selectedPdServiceId+"&pdServiceAndIsReq.pdServiceVersionLinks="+selectedVersionId; 요구사항_현황_데이터_테이블($("#selected_pdService").val(), 요구사항_호출_주소); ////////////////////////////////////////////////////////// } } }); } function statisticsMonitor(pdservice_id, pdservice_version_id) { console.log("[ analysisScope :: statisticsMonitor ] :: pdservice_id => " + pdservice_id); console.log("[ analysisScope :: statisticsMonitor ] :: pdservice_version_id => " + pdservice_version_id); //1. 좌상 게이지 차트 및 타임라인 //2. Time ( 작업일정 ) - 버전 개수 삽입 $.ajax({ url: "/auth-user/api/arms/pdService/getNodeWithVersionOrderByCidDesc?c_id=" + pdservice_id, type: "GET", contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, async: false, statusCode: { 200: function (json) { pdServiceData = json; console.log("[ analysisScope :: statisticsMonitor ] pdServiceData"); console.log(json); let versionData = json.pdServiceVersionEntities; } } }); } function getRelationJiraIssueByPdServiceAndVersions(pdServiceLink, pdServiceVersionLinks) { $.ajax({ url: "/auth-admin/api/arms/analysis/scope/pdService/pdServiceVersionLinks", type: "POST", data:JSON.stringify({ "pdServiceAndIsReq": { "pdServiceLink": pdServiceLink, "pdServiceVersionLinks": pdServiceVersionLinks.split(',').map(Number) } }), contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, async: false, statusCode: { 200: function (data) { // 버전 선택 시 데이터 파싱 setTimeout(function () { console.log("[ analysisScope :: getRelationJ :: iraIssueByPdServiceAndVersions ] 네트워크차트 그리기"); networkChart(pdServiceVersionLinks, data); }, 1500); } } }); } //네트워크차트 function networkChart(pdServiceVersions, jiraIssueData) { $('.network-graph').removeClass('show'); d3.select("#NETWORK_GRAPH").remove(); var NETWORK_DATA = { nodes: [], links: [] }; pdServiceData.id = "pdService-"+ pdServiceData.c_id; pdServiceData.type = "pdService"; NETWORK_DATA.nodes.push(pdServiceData); var 선택한버전 = pdServiceVersions.split(",").map(Number); console.log(" networkChart :: 선택한버전 => " + 선택한버전); var versionList = pdServiceData.pdServiceVersionEntities; versionList.forEach((item) => { if (선택한버전.includes(item.c_id)) { item.id = "version-"+item.c_id; item.type = "version"; NETWORK_DATA.nodes.push(item); // 버전 노드 삽입 console.log(typeof item.id); var link = { source: item.id, target: pdServiceData.id }; NETWORK_DATA.links.push(link); } }); var index = {}; // 지라이슈 노드추가 jiraIssueData.forEach(function (item) { NETWORK_DATA.nodes.push(item); index[item.key] = item; }); // 지라이슈 링크추가 jiraIssueData.forEach(function (item) { if (item.isReq === true) { // 요구사항 이슈일 때 var 버전수 = item.pdServiceVersions.length; if (버전수 && 버전수 > 0) { for (let i =0; i<버전수; i++) { if(선택한버전.includes(item.pdServiceVersions[i])) { var reqToVersionLink = { source: item.id, target: "version-" + item.pdServiceVersions[i] } NETWORK_DATA.links.push(reqToVersionLink); } } } } var parentItem = index[item.parentReqKey]; if (parentItem) { var subtaskToReqLink = { source: item.id, target: parentItem.id } NETWORK_DATA.links.push(subtaskToReqLink); } }); console.log("[ networkChart :: NETWORK_DATA ]"); console.log(NETWORK_DATA); if (NETWORK_DATA.nodes.length === 0) { // 데이터가 없는 경우를 체크 d3.select("#NETWORK_GRAPH").remove(); d3.select(".network-graph").append("p").text("데이터가 없습니다."); return; } // $('#notifyNoVersion').show(); d3.select(".network-graph").append("svg").attr("id", "NETWORK_GRAPH"); /******** network graph config ********/ var networkGraph = { createGraph: function () { let links = NETWORK_DATA.links.map(function (d) { return Object.create(d); }); let nodes = NETWORK_DATA.nodes.map(function (d) { return Object.create(d); }); // 확인 var fillCircle = function (g) { if (g.type === "pdService") { return "#c67cff"; } else if (g.type === "version") { return "rgb(255,127,14)"; } else if (g.isReq === true) { return "rgb(214,39,40)"; } else if (g.isReq === false) { return "rgb(31,119,180)"; } else { return "rgb(44,160,44)"; } }; // 확인 var typeBinding = function (g) { let name = ""; if (g.type === "pdService") { name = "제품(서비스)"; } else if (g.type === "version") { name = "버전"; } else if (g.isReq === true) { name = "요구사항"; } else if (g.isReq === false) { name = "연결된 이슈"; } return "[" + name + "]"; }; var nameBinding = function (g) { let name = ""; if (g.type === "pdService") { return g.c_title; } else if (g.type === "version") { return g.c_title; } else { return g.key; } }; let baseWidth = 400; let baseHeight = 400; let nodeCount = nodes.length; let increaseFactor = Math.floor(nodeCount / 100); if (nodeCount % 100 !== 0) { increaseFactor++; } baseWidth += increaseFactor * 100; baseHeight += increaseFactor * 100; let width = baseWidth; let height = baseHeight; let simulation = d3 .forceSimulation(nodes) .force( "link", d3 .forceLink(links) .id(function (d) { return d.id; }) .distance(30) ) .force("charge", d3.forceManyBody().strength(-1000)) .force("center", d3.forceCenter(width / 3, height / 3)) .force("collide", d3.forceCollide().radius(30)) .force( "radial", d3 .forceRadial() .radius(10) .x(width / 3) .y(height / 3) .strength(0.5) ); //.force("center", d3.forceCenter(width / 3, height / 3)); // .force("collide", d3.forceCollide() // .radius(function(node) { // var linkCount = countLinks(node, links ); // return 5 * linkCount; // }) // ); // .force("collide",d3.forceCollide().radius( function(d){ // console.log(d); // return 20; // })); // simulation.stop(); let svg = d3.select("#NETWORK_GRAPH").attr("viewBox", [0, 0, width, height]); let gHolder = svg.append("g").attr("class", "g-holder"); let link = gHolder .append("g") .attr("fill", "none") .attr("stroke", "gray") .attr("stroke-opacity", 1) .selectAll("path") .data(links) .join("path") .attr("stroke-width", 1.5); /* var node = svg.append("g") .selectAll("circle") .data(nodes) .enter() .append("circle") .attr("r", 8) .attr("fill", color) .call(drag(simulation)); // text 라벨 추가를 위해 g로 그룹핑 node.append("text") .text(function(d){ return d.id }) .style("font-size", "12px") */ let node = gHolder .append("g") .attr("class", "circle-node-holder") .attr("stroke", "white") .attr("stroke-width", 1) .selectAll("g") .data(nodes) .enter() .append("g") .each(function (d) { d3.select(this).append("circle").attr("r", 10).attr("fill", fillCircle(d)); /*d3.select(this) .append("text").text(d.id) .attr("dy",6) .style("text-anchor","middle") .attr("class", "node-label");*/ }) .call(networkGraph.drag(simulation)); node .append("text") .attr("x", 11) .attr("dy", ".31em") .style("font-size", "9px") .style("fill", (d) => fillCircle(d)) .style("font-weight", "5") .attr("stroke", "white") .attr("stroke-width", "0.3") .text((d) => typeBinding(d)); node .append("text") .attr("x", 12) .attr("dy", "1.5em") .style("font-family", "Arial") .style("font-size", "10px") .style("font-weight", "10") .text((d) => nameBinding(d)); simulation.on("tick", function () { link .attr("x1", function (d) { return d.source.x; }) .attr("y1", function (d) { return d.source.y; }) .attr("x2", function (d) { return d.target.x; }) .attr("y2", function (d) { return d.target.y; }); /*node .attr("cx", function(d){ return d.x; }) .attr("cy", function(d){ return d.y; });*/ //circle 노드에서 g 노드로 변경 node.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }); link.attr("d", function (d) { var dx = d.target.x - d.source.x, dy = d.target.y - d.source.y, dr = Math.sqrt(dx * dx + dy * dy); return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; }); simulation.on("end", () => { // 노드 위치의 최소값과 최대값을 구합니다. let xExtent = d3.extent(nodes, d => d.x); let yExtent = d3.extent(nodes, d => d.y); // 노드 위치에 따라 차트의 크기를 설정합니다. let chartWidth = xExtent[1] - xExtent[0] + 20; let chartHeight = yExtent[1] - yExtent[0] + 20; // 화면 크기에 따라 초기 줌 레벨을 설정합니다. let scaleX = width / chartWidth; let scaleY = height / chartHeight; let initScale = Math.min(scaleX, scaleY); console.log("initScale : " + initScale); let widthPoint = width /3; let heightPoint = height / 3; if(initScale > 1) { initScale = 1; widthPoint = width/5; heightPoint = height/5; } let initialTransform = d3.zoomIdentity .translate(widthPoint, heightPoint) // 초기 위치 설정 .scale(initScale); // 초기 줌 레벨 설정 let zoom = d3 .zoom() .scaleExtent([0.1, 5]) // 줌 가능한 최소/최대 줌 레벨 설정 .on("zoom", zoomed); // 줌 이벤트 핸들러 설정 // SVG에 확대/축소 기능 적용 svg.call(zoom); // 초기 줌 설정 svg.transition().duration(500).call(zoom.transform, initialTransform); $('.network-graph').addClass('show'); }); }); function zoomed() { // 현재 확대/축소 변환을 얻음 let transform = d3.event.transform; // 모든 노드 및 링크를 현재 확대/축소 변환으로 이동/확대/축소 gHolder.attr("transform", transform); } //invalidation.then(() => simulation.stop()); // $('.network-graph').addClass('show'); return svg.node(); }, drag: function (simulation) { function dragstarted(d) { if (!d3.event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragended(d) { if (!d3.event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended); } }; /******** network graph create ********/ networkGraph.createGraph(); } function formatDate(date) { var year = date.getFullYear().toString().slice(-2); // 연도의 마지막 두 자리를 얻습니다. var month = (date.getMonth() + 1).toString().padStart(2, "0"); var day = date.getDate().toString().padStart(2, "0"); return year + "-" + month + "-" + day; } ///////////////////////////////////////////////////////// // Circular Packing Chart ///////////////////////////////////////////////////////// function getReqStatusAndInvolvedAssignees(pdServiceLink, pdServiceVersionLinks) { // "?pdServiceAndIsReqDto.pdServiceId=31313&pdServiceAndIsReqDto.pdServiceVersions=1&pdServiceAndIsReqDto.pdServiceVersions=2" -H "accept: */*" $.ajax({ url: "/auth-admin/api/arms/analysis/scope/req-status-and-reqInvolved-unique-assignees-per-version", type: "POST", contentType: "application/json;charset=UTF-8", dataType: "json", data: JSON.stringify({ "pdServiceAndIsReq": { "isReq": true, "pdServiceLink": pdServiceLink, "pdServiceVersionLinks": pdServiceVersionLinks.split(',').map(Number) } }), progress: true, async: false, statusCode: { 200: function (result) { console.log("[ analysisScope :: getReqStatusAndInvolvedAssignees ] :: result"); console.log(result); let pdServiceName; pdServiceListData.forEach(elements => { if (elements["pdServiceId"] === +pdServiceLink) { pdServiceName = elements["pdServiceName"]; } }); let dataObject = {}; let responseResult = result["response"]; for (let i = 0; i < responseResult.length; i++) { // 버전이름 가져오기 let element = responseResult[i]; let versionName = element["versionName"].replaceAll(".","_"); let detailElement = element["reqStatusesByVersionWithAssignee"]; let verSubObject = {"$count" : detailElement["numOfWorkers"], "$status" : detailElement["statusName"]}; if (dataObject[versionName]===undefined) { dataObject[versionName] = {}; } dataObject[versionName][detailElement["issueKey"]] = verSubObject; } drawCircularPacking("circularPacking",pdServiceName,dataObject); } } }); } ///////////////////////////////////////////////////////// // Radial Polar Bar Chart - 제품(서비스)의 버전별 요구사항 수 ///////////////////////////////////////////////////////// function getReqPerMappedVersions(pdServiceLink, pdServiceVersionLinks) { let reqAddUrl = "/T_ARMS_REQADD_"+ pdServiceLink +"/getReqAddListByFilter?"; $.ajax({ url: "/auth-admin/api/arms/analysis/scope/req-per-version" +reqAddUrl, type: "POST", data: JSON.stringify({ "pdServiceAndIsReq": { "pdServiceLink": pdServiceLink, "pdServiceVersionLinks": pdServiceVersionLinks.split(',').map(Number) } }), contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, async: false, statusCode: { 200: function (result) { console.log("[ analysisScope :: getReqPerMappedVersions ] :: result"); console.log(result); let colorArr = ColorPalette.eCharts.radialPolarBarChart; drawRadialPolarBarChart("reqPerVersionRoseChart", result["response"], colorArr); } } }); } function chord(data) { const $container = document.getElementById("circular_sankey"); const $chart = makeChart(data); $container.append($chart); } function makeChart(data) { const width = 640; const height = width; const outerRadius = Math.min(width, height) * 0.5 - 30; const innerRadius = outerRadius - 20; const { names, colors } = data; const sum = d3.sum(data.flat()); const tickStep = d3.tickStep(0, sum, 100); const tickStepMajor = d3.tickStep(0, sum, 20); const formatValue = d3.formatPrefix(",.0", tickStep); const chord = d3 .chord() .padAngle(20 / innerRadius) .sortSubgroups(d3.descending); const arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius); const ribbon = d3.ribbon().radius(innerRadius); const svg = d3 .create("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [-width / 2, -height / 2, width, height]) .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;"); const chords = chord(data); const group = svg.append("g").selectAll().data(chords.groups).join("g"); group .append("path") .attr("fill", (d) => colors[d.index]) .attr("d", arc) .append("title") .text((d) => `${d.value.toLocaleString("en-US")} ${names[d.index]}`); const groupTick = group .append("g") .selectAll() .data((d) => groupTicks(d, tickStep)) .join("g") .attr("transform", (d) => `rotate(${(d.angle * 180) / Math.PI - 90}) translate(${outerRadius},0)`); groupTick.append("line").attr("stroke", "currentColor").attr("x2", 6); groupTick .filter((d) => d.value % tickStepMajor === 0) .append("text") .attr("x", 8) .attr("dy", ".35em") .attr("transform", (d) => (d.angle > Math.PI ? "rotate(180) translate(-16)" : null)) .attr("text-anchor", (d) => (d.angle > Math.PI ? "end" : null)) .text((d) => formatValue(d.value)); svg .append("g") .attr("fill-opacity", 0.7) .selectAll() .data(chords) .join("path") .attr("d", ribbon) .attr("fill", (d) => colors[d.target.index]) .attr("opacity", 0.6) .on("mouseenter", function (d) { d3.select(this).transition().attr("opacity", 1); }) .on("mouseout", function () { d3.select(this).transition().attr("opacity", 0.6); }) .attr("stroke", "white") .append("title") .text( (d) => `${d.source.value.toLocaleString("en-US")} ${names[d.source.index]} → ${names[d.target.index]}${ d.source.index !== d.target.index ? `\n${d.target.value.toLocaleString("en-US")} ${names[d.target.index]} → ${names[d.source.index]}` : `` }` ); return svg.node(); } function groupTicks(d, step) { const k = (d.endAngle - d.startAngle) / d.value; return d3.range(0, d.value, step).map((value) => { return { value: value, angle: value * k + d.startAngle }; }); } function treeBar() { $.ajax({ url: "/auth-admin/api/arms/analysis/scope/tree-bar-top10", type: "POST", data:JSON.stringify({ "pdServiceAndIsReq": { "pdServiceLink": selectedPdServiceId, "pdServiceVersionLinks": selectedVersionId.split(',').map(Number), "isReq":false } }), contentType: "application/json;charset=UTF-8", dataType: "json", progress: true, async: false }) .done(function (apiResponse) { const data = apiResponse.response; console.log("[ analysisScope :: treeBar ] data"); console.log(data); d3.select("#tree_bar_container svg").selectAll("*").remove(); let assigneeData = data.filter(item => item.type === 'assignee'); if(assigneeData.length === 0) { return; } if (assigneeData.length === 1 && assigneeData[0].name === "No Data") { return; } let maxValue = Math.max(...assigneeData.map(item => item.value)); renderTreeBar(data, assigneeData, maxValue); }) .fail(function (e) {}) .always(function () {}); } function renderTreeBar(data, assigneeData, maxValue) { const charts = document.getElementById("tree_bar_container"); const $container = document.getElementById("tree_bar_container"), width = $container.offsetWidth, height = $container.offsetHeight, svg = d3.select("#tree_bar_container svg"), g = svg.append("g").attr("transform", "translate(10,10)").attr('width', width) .attr('height', height) .call(responsiveTreeBar), experienceName = Array(maxValue).fill("").map((_, i) => (i+1 === maxValue ? maxValue.toString() : "")), formatSkillPoints = function (d) { return experienceName[d % maxValue]; }, xScale = d3.scaleLinear().domain([0, maxValue]).range([0, 100]), xAxis = d3.axisTop().scale(xScale).ticks(maxValue).tickFormat(formatSkillPoints), tree = d3.cluster().size([height, width - 145]), stratify = d3 .stratify() .id((d) => d.id) .parentId((d) => d.parent), root = stratify(data); tree(root); const link = g .selectAll(".link") .data(root.descendants().slice(1)) .enter() .append("path") .attr("class", "link") .attr("d", function (d) { return `M${d.y},${d.x}C${d.parent.y + 100},${d.x} ${d.parent.y + 100},${d.parent.x} ${d.parent.y},${d.parent.x}`; }); const node = g .selectAll(".node") .data(root.descendants()) .enter() .append("g") .attr("class", function (d) { return `node${d.children ? " node--internal" : " node--leaf"}`; }) .attr("transform", function (d) { return `translate(${d.y}, ${d.x})`; }); node.append("circle").attr("r", 4); const leafNodeG = g .selectAll(".node--leaf") .append("g") .attr("class", "node--leaf-g") .attr("transform", "translate(" + 8 + "," + -13 + ")"); leafNodeG .append("rect") .style("fill", function (d) { return d.data.color; }) .attr("width", 2) .attr("height", 20) .attr("rx", 2) .attr("ry", 2) .transition() .duration(800) .attr("width", function (d) { return xScale(d.data.value); }); leafNodeG .append("text") .attr("dy", 14) .attr("x", 8) .style("text-anchor", "start") .text(function (d) { return d.data.name; }); const internalNode = g.selectAll(".node--internal"); internalNode .append("text") .attr("class", (d) => d.data.id === "1" && "root") .attr("y", -10) .style("text-anchor", "middle") .text(function (d) { return d.data.name; }); const firstEndNode = g.select(".node--leaf"); firstEndNode .insert("g") .attr("class", "xAxis") .attr("transform", "translate(" + 7 + "," + -14 + ")") .call(xAxis); firstEndNode .insert("g") .attr("class", "grid") .attr("transform", "translate(7," + (height - 15) + ")") .call(d3.axisBottom().scale(xScale).ticks(5).tickSize(-height, 0, 0).tickFormat("")); svg.selectAll(".grid").select("line").style("stroke-dasharray", "1,1").style("stroke", "white"); responsiveTreeBar(svg); } function responsiveTreeBar(svg) { const container = d3.select(svg.node().parentNode), width = parseInt(svg.style('width'), 10), height = parseInt(svg.style('height'), 10), aspect = width / height; svg.attr('viewBox', `0 0 ${width} ${height}`) .attr('preserveAspectRatio', 'xMinYMid') .call(resize); d3.select(window).on( 'resize.' + container.attr('id'), resize ); function resize() { const w = parseInt(container.style('width')) + 50; svg.attr('width', w); svg.attr('height', Math.round(w / aspect)); } } //////////////////////////////////////////////////////////////////////////////////////// //요구사항 현황 데이터 테이블 //////////////////////////////////////////////////////////////////////////////////////// function 요구사항_현황_데이터_테이블(selectId, endPointUrl) { var columnList = [ { name: "reqAddEntity.pdServiceEntity.c_id", title: "제품(서비스) 아이디", data: "pdServiceEntity.c_id", visible: false, defaultContent: 'N/A' }, { name: "version_name", title: "버전", data: "version_name", render: function (data, type, row, meta) { if (isEmpty(data) || data === "unknown") { return "