Index: arms/js/analysis/Cost/circularPackingChart.js =================================================================== diff -u --- arms/js/analysis/Cost/circularPackingChart.js (revision 0) +++ arms/js/analysis/Cost/circularPackingChart.js (revision d6a99e85daaac57969208e01aac1894c7cff39b5) @@ -0,0 +1,313 @@ +function drawCircularPacking(target, psServiceName,rawData, colorArr) { + var chartDom = document.getElementById(target); + var myChart = echarts.init(chartDom); + var option; + // ChartWithFooter 관련 + let reqCount = 0; // total + let statusCounts = {}; + let statusDataArr = []; + + var colorPalette = [ //e chart 컬러 팔레트 + '#d48265','#91c7ae', + '#749f83','#ca8622','#bda29a','#6e7074','#546570', + '#c4ccd3' + ]; + + if(rawData) { + run(rawData); + } + + function run(rawData) { + const dataWrap = prepareData(rawData); + + console.log(dataWrap); + + initChart(dataWrap.seriesData, dataWrap.maxDepth); + } + function prepareData(rawData) { + const seriesData = []; + let maxDepth = 0; + let totalValue = 0; + + function convert(source, basePath, depth) { + let value = 0; + + if(Array.isArray(source)) { + source.forEach(item => { + for(let key in item) { + if(Array.isArray(item[key])) { + let subValue = 0; + item[key].forEach(subItem => { + subValue += subItem.cost; + if(subValue !== 0) { + seriesData.push({ + id: basePath + '.' + key + '.' + subItem.project, + value: subItem.cost, + depth: depth + 2, + index: seriesData.length + }); + } + }); + if(subValue !== 0) { + value += subValue; + seriesData.push({ + id: basePath + '.' + key, + value: subValue, + depth: depth + 1, + index: seriesData.length + }); + } + } + } + }); + } + + if(value !== 0 || depth === 0) { + seriesData.push({ + id: basePath, + value: value, + depth: depth, + index: seriesData.length + }); + if(depth === 0) { + totalValue = value; + } + } + + maxDepth = Math.max(maxDepth, depth); + + for (var key in source) { + if (source.hasOwnProperty(key) && !key.match(/^\$/)) { + var path = basePath + '.' + key; + if (typeof source[key] === 'object' && source[key] !== null) { + convert(source[key], path, depth + 1); + } + } + } + } + + convert(rawData, psServiceName, 0); + seriesData[0].value = totalValue; + + return { + seriesData: seriesData, + maxDepth: maxDepth + }; + } + + function initChart(seriesData, maxDepth) { + console.log("seriesData ===> ") + console.log(seriesData); + var displayRoot = stratify(); + function stratify() { + return d3 + .stratify() + .parentId(function (d) { + return d.id.substring(0, d.id.lastIndexOf('.')); + })(seriesData) + .sum(function (d) { + return d.value || 0; + }) + .sort(function (a, b) { + return b.value - a.value; + }); + } + function overallLayout(params, api) { + var context = params.context; + d3 + .pack() + .size([api.getWidth() - 2, api.getHeight() - 2]) + .padding(3)(displayRoot); + context.nodes = {}; + displayRoot.descendants().forEach(function (node, index) { + context.nodes[node.id] = node; + }); + } + function renderItem(params, api) { + var context = params.context; + // Only do that layout once in each time `setOption` called. + if (!context.layout) { + context.layout = true; + overallLayout(params, api); + } + var nodePath = api.value('id'); + var node = context.nodes[nodePath]; + if (!node) { + // Reder nothing. + return; + } + var isLeaf = !node.children || !node.children.length; + var focus = new Uint32Array( + node.descendants().map(function (node) { + return node.data.index; + }) + ); + var nodeName = isLeaf + ? nodePath + .slice(nodePath.lastIndexOf('.') + 1) + .split(/(?=[A-Z][^A-Z])/g) + .join('\n') + : ''; + var z2 = api.value('depth') * 2; + return { + type: 'circle', + focus: focus, + shape: { + cx: node.x, + cy: node.y, + r: node.r + }, + transition: ['shape'], + z2: z2, + textContent: { + type: 'text', + style: { + // transition: isLeaf ? 'fontSize' : null, + text: nodeName, + fontFamily: 'Arial', + width: node.r * 1.3, + overflow: 'truncate', + fontSize: node.r / 3 + }, + emphasis: { + style: { + overflow: null, + fontSize: Math.max(node.r / 3, 12) + } + } + }, + textConfig: { + position: 'inside' + }, + style: { + fill: api.visual('color') + }, + emphasis: { + style: { + fontFamily: 'Arial', + fontSize: 12, + shadowBlur: 20, + shadowOffsetX: 3, + shadowOffsetY: 5, + shadowColor: 'rgba(0,0,0,0.3)' + } + } + }; + } + + option = { + dataset: { + source: seriesData + }, + tooltip: { + confine: true + }, + visualMap: [ + { + show: false, + min: 0, + max: maxDepth, + dimension: 'depth', + inRange: {} // 색 일괄 지정을 하지 않아도. 빈값으로 두어야 합니다. + } + ], + hoverLayerThreshold: Infinity, + series: { + type: 'custom', + renderItem: renderItem, + progressive: 0, + coordinateSystem: 'none', + itemStyle: { + color: function(params) { + if (params.data.value) { + return colorPalette[params.value.depth]; + } else { + return "rgba(55,125,184,0.62)"; + } + } + }, + tooltip: { + formatter: function(params) { + // params.value에는 원본 값이 들어있을 것입니다. 여기에 단위를 붙여 반환하면 됩니다. + if(params.data.value) { + return `${params.data.id}
+ - 비용 : ${params.data.value} `; + } else { + return `${params.data.id}`; + } + } + } + }, + graphic: [ + { + type: 'group', + left: 20, + top: 20, + children: [ + { + type: 'text', + z: 100, + left: 0, + top: 0, + style: { + text: [ + '{a| 제품 비용 }', + '{a| ' + reqCount + '}' + ].join('\n'), + rich: { + a: { + fontSize: 13, + fontWeight: 'bold', + lineHeight: 20, + fontFamily: 'Arial', + fill: 'white' + } + } + } + } + ] + } + ] + }; + + option && myChart.setOption(option, true); + myChart.on('click', { seriesIndex: 0 }, function (params) { + drillDown(params.data.id); + }); + + function drillDown(targetNodeId) { + displayRoot = stratify(); + if (targetNodeId != null) { + displayRoot = displayRoot.descendants().find(function (node) { + return node.data.id === targetNodeId; + }); + } + // A trick to prevent d3-hierarchy from visiting parents in this algorithm. + displayRoot.parent = null; + myChart.setOption({ + dataset: { + source: seriesData + } + }); + } + // Reset: click on the blank area. + myChart.getZr().on('click', function (event) { + if (!event.target) { + drillDown(); + } + }); + } + + function replaceNaN(value) { + if (isNaN(value)) { + return " - "; + } else { + return value; + } + } + + window.addEventListener('resize', function () { + myChart.resize(); + }); +} + Index: arms/js/analysisCost.js =================================================================== diff -u -ra12104df4d666eb2a67608cb998c3b6d9589d68a -rd6a99e85daaac57969208e01aac1894c7cff39b5 --- arms/js/analysisCost.js (.../analysisCost.js) (revision a12104df4d666eb2a67608cb998c3b6d9589d68a) +++ arms/js/analysisCost.js (.../analysisCost.js) (revision d6a99e85daaac57969208e01aac1894c7cff39b5) @@ -58,7 +58,10 @@ "../reference/jquery-plugins/d3-5.16.0/d3.min.js", // 생성한 차트 import "js/analysis/topmenu/basicRadar.js", - "js/analysis/topmenu/topMenu.js" + "js/analysis/topmenu/topMenu.js", + + //CirclePacking with d3 Chart + "js/analysis/Cost/circularPackingChart.js" ], [ "../reference/jquery-plugins/dataTables-1.10.16/media/css/jquery.dataTables_lightblue4.css", @@ -176,6 +179,9 @@ // 요구사항 및 연결이슈 통계 getReqAndLinkedIssueData(selectedPdServiceId, selectedVersionId); + //Circular Packing with D3 차트 + getReqCostRatio(selectedPdServiceId, versionTag); + 버전별_요구사항별_인력정보가져오기(selectedPdServiceId, selectedVersionId); //요구사항 현황 데이터 테이블 로드 // console.log(" ============ makeVersionMultiSelectBox ============= "); @@ -216,13 +222,11 @@ // 요구사항 및 연결이슈 통계 getReqAndLinkedIssueData(selectedPdServiceId, selectedVersionId); // Circular Packing with D3 차트 - // getReqStatusAndAssignees(selectedPdServiceId, selectedVersionId); - + getReqCostRatio(selectedPdServiceId, pdServiceVersionIds); // 투자 대비 소모 비용 차트 compareCostsChart(selectedPdServiceId, selectedVersionId); // 수익 현황 차트 incomeStatusChart(); - 버전별_요구사항별_인력정보가져오기(selectedPdServiceId, selectedVersionId); @@ -405,7 +409,91 @@ ] });*/ } +///////////////////////////////////////////////////////// +// 요구사항 단가 기반 크기 확인 차트 +///////////////////////////////////////////////////////// +function getReqCostRatio(pdServiceLink, pdServiceVersionLinks) { + let paramData = { + "요구_사항" : { + 'isReqType': 'REQUIREMENT', + 'pdServiceLink' : selectedPdServiceId, + 'pdServiceVersionLinks' : pdServiceVersionLinks,//[16,17,18] + '메인그룹필드' : 'pdServiceVersion', + '컨텐츠보기여부' : false, + '크기' : 10000, + '하위그룹필드들' : ['key','assignee.assignee_emailAddress.keyword'], + '하위크기' : 10000 + }, + "하위_이슈_사항" : { + 'isReqType': 'ISSUE', + 'pdServiceLink': selectedPdServiceId, + 'pdServiceVersionLinks': pdServiceVersionLinks, + '메인그룹필드': 'parentReqKey', + '컨텐츠보기여부': false, + '크기': 10000, + '하위그룹필드들': ['assignee.assignee_emailAddress.keyword'],//'[assignee.assignee_emailAddress.keyword]', + '하위크기': 10000 + } + } + $.ajax({ + url: "/auth-user/api/arms/analysis/scope/req-status-and-reqInvolved-unique-assignees", + type: "POST", + contentType: "application/json;charset=UTF-8", + dataType: "json", + data: JSON.stringify(paramData), + progress: true, + statusCode: { + 200: function (result) { +// console.log("[ analysisScope :: getReqCostRatio ] :: result"); +// console.log(result); + let pdServiceName; + pdServiceListData.forEach(elements => { + if (elements["pdServiceId"] === +pdServiceLink) { + pdServiceName = elements["pdServiceName"]; + } + }); + + let data = { + "1_0_1": [ + { + "요구사항": [ + { "project": "TE-1", "cost": 400 }, + { "project": "TE-2", "cost": 400 }, + { "project": "TE-3", "cost": 400 } + ] + }, + { + "요구사항2": [ + { "project": "TT-1", "cost": 300 }, + { "project": "TT-2", "cost": 300 }, + { "project": "TT-3", "cost": 300 } + ] + } + ], + "1_0_2": [ + { + "요구사항3": [ + { "project": "TE-4", "cost": 100 }, + { "project": "TE-5", "cost": 100 }, + { "project": "TE-6", "cost": 100 } + ] + }, + { + "요구사항4": [ + { "project": "TT-4", "cost": 300 }, + { "project": "TT-5", "cost": 300 }, + { "project": "TT-6", "cost": 300 } + ] + } + ] + }; + drawCircularPacking("circularPacking",pdServiceName,data); + } + } + }); +} + ///////////////////////////////////////////////////////// // 요구사항 별 수익 현황 그래프 /////////////////////////////////////////////////////////