Open kirin-ri opened 6 months ago
<html>
<body>
<!--StartFragment-->
Linux version 6.2.0-1014-azure (buildd@lcy02-amd64-030) (x86_64-linux-gnu-gcc-11 (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #14~22.04.1-Ubuntu SMP Wed Sep 13 16:15:26 UTC 2023
--
<!--EndFragment-->
</body>
</html>
import cytoscape, { Collection, NodeCollection } from 'cytoscape';
import cxtmenu from 'cytoscape-cxtmenu';
import dagre from 'cytoscape-dagre';
// @ts-ignore
import cyHtmlLabel from 'cytoscape-node-html-label';
import { useEffect, useRef, useState } from 'react';
import { commonAjax } from '../../../components/commonAjax';
cytoscape.use(dagre);
cytoscape.use(cxtmenu);
cytoscape.use(cyHtmlLabel);
function digitalValueTree() {
const graphContainer = useRef<HTMLDivElement>(null);
const [dispElements, setDispElements] = useState<[]>([]);
const [elementsStyle, setElementsStyle] = useState<[]>([]);
const [contentHeight, setContentHeight] = useState<number | string>(0);
const [refreshFlg, setRefreshFlg] = useState<boolean>(true);
const [perspectiveCheck, setPerspectiveCheck] = useState<boolean>(true);
const [simulationCheck, setSimulationCheck] = useState<boolean>(false);
const [targetMetrics, setTargetMetrics] = useState<string>('');
let tappedBefore: null;
let tappedTimeout: string | number | NodeJS.Timeout | undefined;
// 視点フィルター変更時イベント
function changePerspective() {
const vals: string[] = [];
$('input[class="form-check-input"]:checked').each(function () {
vals.push($(this)[0].id);
});
const req = { perspectives: vals };
(async () => {
commonAjax
.axios({ loading: true })
.post('/api/digitalValueTreeInfo', req)
.then((res) => {
setDispElements(res.data.elements);
setElementsStyle(res.data.style);
setRefreshFlg(!refreshFlg);
});
})();
}
// シミュレーションモード変更時変更時イベント
function changeSimulationMode() {
const CheckBox = document.querySelector(
"input[type='checkbox']#simulationCheckBox",
) as HTMLInputElement;
setSimulationCheck(CheckBox.checked);
setRefreshFlg(!refreshFlg);
}
// 全画面モード選択時イベント
function fullscreen() {
document.getElementById('contentBody')!.requestFullscreen();
// setContentHeight(window.outerHeight);
// setContentHeight('100%');
// setRefreshFlg(!refreshFlg);
}
// フルスクリーン切替時イベント
function fullscreenchanged() {
if (document.fullscreenElement) {
// setContentHeight('100%');
setContentHeight(window.outerHeight);
setRefreshFlg(!refreshFlg);
// document.getElementById('contentBody')!.style.backgroundColor = 'white';
} else {
setContentHeight(
window.innerHeight -
document.getElementsByClassName('main-header')[0].clientHeight -
document.getElementsByClassName('page-cover')[0].clientHeight -
document.getElementsByClassName('content-header')[0].clientHeight -
42,
);
setRefreshFlg(!refreshFlg);
// document.getElementById('contentBody')!.style.backgroundColor =
// 'var(--content-bg-color)';
}
}
// フルスクリーン切替イベントリスナー
document.addEventListener('fullscreenchange', fullscreenchanged);
useEffect(() => {
(async () => {
const vals = [] as any;
const req = { perspectives: vals };
commonAjax
.axios({ loading: true })
.post('/api/digitalValueTreeInfo', req)
.then((res) => {
setDispElements(res.data.elements);
setElementsStyle(res.data.style);
setContentHeight(
window.innerHeight -
document.getElementsByClassName('main-header')[0].clientHeight -
document.getElementsByClassName('page-cover')[0].clientHeight -
document.getElementsByClassName('content-header')[0]
.clientHeight -
42,
);
setRefreshFlg(!refreshFlg);
});
})();
}, []);
useEffect(() => {
if (graphContainer.current) {
// cytoscape init
const cy = cytoscape({
wheelSensitivity: 0.1, // マウスホイール拡大縮小感度
container: graphContainer.current,
elements: dispElements,
style: elementsStyle,
});
// ノード幅動的設定
const labelWidth = (node: NodeCollection): number => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
context.font = ['style', 'weight', 'size', 'family']
.map((style) => node.style(`font-${style}`))
.join(' ');
const { width } = context.measureText(node.data('label'));
const padding = 20;
return width + padding;
};
// シュミレーションモード分岐
if (simulationCheck) {
/* *********************************
ここにシュミレーション処理追加!!!
********************************* */
// sample-ノードのラベルにHTMLを表示するための設定
// @ts-ignore
cy.nodeHtmlLabel(
[
{
query: 'node[group="metrics"]',
halign: 'center',
valign: 'center',
tpl: (
data: any,
) => `<table style="border: 2px solid rgb(140 140 140);">
<thead>
<tr>
<th scope="col" style="border: 1px solid rgb(160 160 160)">head1</th>
<th scope="col" style="border: 1px solid rgb(160 160 160)">head2</th>
</tr>
</thead>
<tbody>
<tr>
<th style="border: 1px solid rgb(160 160 160)">col1</th>
<td style="border: 1px solid rgb(160 160 160)">col2</td>
</tr>
</tbody>
</table>
`,
},
// {
// query: "node[id='metrics2']",
// halign: 'center',
// valign: 'center',
// tpl: (data: any) =>
// `<div style="background-color: red; border-radius:10px; padding:5; margin:0 0 45 130;">badge</div>`,
// },
// {
// query: "node[id='metrics3']",
// halign: 'center',
// valign: 'center',
// tpl: (data: any) => `<input id="sssss" style="margin:50 0 0 0;"/>`,
// },
],
// {
// enablePointerEvents: true,
// },
);
cy.nodes("not(node[group='metrics'])").forEach((node) => {
node.style('width', labelWidth(node));
});
cy.nodes('node[group="metrics"]').forEach((ele) => {
ele.style('width', '140');
ele.style('height', '150');
ele.style('text-valign', 'top');
ele.style('text-halign', 'center');
});
} else {
// cxtmenu
const metricsCxtmenu = {
selector: 'node[group="metrics"]',
menuRadius: 75,
activePadding: 8,
openMenuEvents: 'tap',
// openMenuEvents: 'cxttapstart',
// openMenuEvents: 'cxttapstart taphold',
outsideMenuCancel: 8,
menuItemTextSize: 100,
commands: (ele: Collection) => [
{
content: '詳細',
contentStyle: {},
enabled: ele.data('logical_name') !== null,
select(element: any) {
const openNewTab = (url: string) => {
const newWindow = window.open(url, '_blank');
if (newWindow) {
newWindow.opener = null;
}
};
const targetId = element.data('logical_name');
openNewTab(`/#/scm-metrics-details/${targetId}`);
// history.push(`/scm-metrics-details/${targetId}`);//新規タブで開かない場合
},
},
{
content: 'リレーションハイライト',
contentStyle: {},
enabled: true,
select(element: any) {
cy.elements().removeClass('unhighlighted'); // 既存のクラスをクリア
cy.elements().removeClass('highlighted'); // 既存のクラスをクリア
// element.outgoers().addClass('highlighted'); // 接続されているエッジとノードをハイライト
element.predecessors().addClass('highlighted'); // 上流エッジとノードをハイライト
element.successors().addClass('highlighted'); // 下流エッジとノードをハイライト
cy.elements()
// .not(element.outgoers())
.not(element.predecessors()) // 上流
.not(element.successors()) // 下流
.not(element) // 選択ノード
.not('node[group="hidden_item"]') // 選択ノード
.not('node[group="perspective"]') // 選択ノード
.addClass('unhighlighted'); // それ以外をグレーアウト
},
},
{
content: 'シミュレーション',
contentStyle: {},
enabled: false,
},
{
content: '参照データ',
contentStyle: {},
enabled: ele.data('logical_name') !== null,
select(element: any) {
setTargetMetrics(element.data('id'));
},
},
],
};
const otherCxtmenu = {
selector:
'node[group="financial_parent"],node[group="financial_child"],node[group="pl"],node[group="bs"],node[group="reform"],node[group="measure_category"],node[group="measure"]',
menuRadius: 75,
activePadding: 8,
openMenuEvents: 'tap',
// openMenuEvents: 'cxttapstart',
// openMenuEvents: 'cxttapstart taphold',
outsideMenuCancel: 8,
menuItemTextSize: 100,
commands: [
{
content: 'リレーションハイライト',
contentStyle: {},
enabled: true,
select(element: any) {
cy.elements().removeClass('unhighlighted'); // 既存のクラスをクリア
cy.elements().removeClass('highlighted'); // 既存のクラスをクリア
// element.outgoers().addClass('highlighted'); // 接続されているエッジとノードをハイライト
element.predecessors().addClass('highlighted'); // 上流エッジとノードをハイライト
element.successors().addClass('highlighted'); // 下流エッジとノードをハイライト
cy.elements()
// .not(element.outgoers())
.not(element.predecessors()) // 上流
.not(element.successors()) // 下流
.not(element) // 選択ノード
.not('node[group="hidden_item"]') // 選択ノード
.not('node[group="plBs"]') // 選択ノード
.not('node[group="perspective"]') // 選択ノード
.addClass('unhighlighted'); // それ以外をグレーアウト
},
},
],
};
cy.cxtmenu(metricsCxtmenu);
cy.cxtmenu(otherCxtmenu);
// canvasクリック時イベント
// cy.on('tap', () => {
// cy.elements().removeClass('highlighted unhighlighted'); // 選択解除時にクラスをクリア
// });
// ダブルクリックでハイライトクラスクリアver
cy.on('tap', (event) => {
// NOTE: older cytoscape's event has cyTarget rather than target.
const tappedNow = event.target;
if (tappedTimeout && tappedBefore) {
clearTimeout(tappedTimeout);
}
if (tappedBefore === tappedNow) {
tappedNow.trigger('doubleTap');
tappedBefore = null;
} else {
tappedTimeout = setTimeout(() => {
tappedBefore = null;
}, 300);
tappedBefore = tappedNow;
}
});
cy.on('doubleTap', () => {
cy.elements().removeClass('highlighted unhighlighted'); // 選択解除時にクラスをクリア
});
cy.nodes().forEach((node) => {
node.style('width', labelWidth(node));
});
}
const layout = {
name: 'dagre',
nodeSep: 50,
edgeSep: 0,
rankSep: 30,
// animate: true,
// padding: 10,
};
cy.layout(layout).run();
return () => {
cy.destroy();
};
}
}, [refreshFlg]);
useEffect(() => {
if (targetMetrics) {
// (async () => {
// const vals = [] as any;
// const req = { perspectives: vals };
// commonAjax
// .axios({ loading: true })
// .get(`/api/digitalValueTreeInfo/{targetMetrics}`)
// .then((res) => {
// setDispElements(res.data.elements);
// setElementsStyle(res.data.style);
// setContentHeight(
// window.innerHeight -
// document.getElementsByClassName('main-header')[0].clientHeight -
// document.getElementsByClassName('page-cover')[0].clientHeight -
// document.getElementsByClassName('content-header')[0]
// .clientHeight -
// 42,
// );
// setRefreshFlg(!refreshFlg);
// });
// })();
const parentDivEle = document.getElementById('apiInfoPanelContents');
const tmpParentDivEle = document.createElement('div');
const tmpChildDivEle = document.createElement('div');
const iEle = document.createElement('i');
tmpParentDivEle.id = targetMetrics;
tmpParentDivEle.className = 'apiInfoPanel';
iEle.className = 'fa fa-times-circle fa-lg float-right';
iEle.ariaHidden = 'true';
iEle.onclick = function () {
document.getElementById(tmpParentDivEle.id)?.remove();
};
tmpParentDivEle.appendChild(iEle);
tmpChildDivEle.textContent = `${targetMetrics}`;
tmpParentDivEle.appendChild(tmpChildDivEle);
setTargetMetrics('');
// parentDivEle?.appendChild(tmpParentDivEle);//最下部に追加するパターン
parentDivEle?.insertBefore(tmpParentDivEle, parentDivEle?.children[0]); // 最上部に追加するパターン
}
}, [targetMetrics]);
return (
<div className="content-wrapper" id="digital-value-tree">
<section className="page-cover">
<h1>Digital value tree</h1>
</section>
<section className="content-header">
<div className="content-header-left">
<h1>Digital value tree</h1>
<div className="content-header-desc">
**************概要**************
</div>
{/* <input type="button" onClick={fullscreen} value="全画面モード" /> */}
{/* <Link to="/metrics-network-tab" className="nav-link" target="_blank">
全画面モード
</Link> */}
</div>
<div className="btn-right btn-lg">
<i className={`fa fa-solid fa-expand`}
aria-hidden="true"
onClick={fullscreen}
>
</i>
</div>
</section>
<section className="content">
<div
id="contentBody"
style={{
backgroundColor: 'var(--content-bg-color)',
}}
>
<div
id="legendContens"
style={{
top: 10,
right: 10,
}}
>
<div>
<ul>
<li id="legend1">財務指標</li>
<li id="legend2">P/L</li>
<li id="legend3">B/S</li>
<li id="legend4">改革指標</li>
<li id="legend5">施策カテゴリー</li>
<li id="legend6">施策</li>
<li id="legend7">KPI例</li>
</ul>
</div>
</div>
<div
id="perspectiveContents"
style={{
top: 200,
right: 10,
}}
>
<div className="form-check">
<input
type="checkbox"
id="scm"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label className="form-check-label" htmlFor="scm">
SCM(需給/調達)視点
</label>
</div>
<div className="form-check">
<input
type="checkbox"
id="logistics"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label className="form-check-label" htmlFor="logistics">
ロジスティックス視点
</label>
</div>
<div className="form-check">
<input
type="checkbox"
id="production_quality_assurance"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label
className="form-check-label"
htmlFor="production_quality_assurance"
>
生産・品質保証視点
</label>
</div>
</div>
<div
id="simulationContents"
style={{
top: 320,
right: 10,
}}
>
<div className="form-check">
<input
type="checkbox"
id="simulationCheckBox"
className="form-check-input"
defaultChecked={simulationCheck}
onChange={changeSimulationMode}
/>
<label className="form-check-label" htmlFor="simulationCheckBox">
シミュレーションモード
</label>
</div>
</div>
<div id="apiInfoPanelContents" />
<div
ref={graphContainer}
id="graphContainer"
style={{
height: contentHeight,
}}
/>
</div>
</section>
</div>
);
}
export default digitalValueTree;
import cytoscape, { Collection, NodeCollection } from 'cytoscape';
import cxtmenu from 'cytoscape-cxtmenu';
import dagre from 'cytoscape-dagre';
import cyHtmlLabel from 'cytoscape-node-html-label';
import { useEffect, useRef, useState } from 'react';
import { commonAjax } from '../../../components/commonAjax';
cytoscape.use(dagre);
cytoscape.use(cxtmenu);
cytoscape.use(cyHtmlLabel);
function DigitalValueTree() {
const graphContainer = useRef<HTMLDivElement>(null);
const [dispElements, setDispElements] = useState<[]>([]);
const [elementsStyle, setElementsStyle] = useState<[]>([]);
const [contentHeight, setContentHeight] = useState<number | string>(0);
const [refreshFlg, setRefreshFlg] = useState<boolean>(true);
const [perspectiveCheck, setPerspectiveCheck] = useState<boolean>(true);
const [simulationCheck, setSimulationCheck] = useState<boolean>(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [targetMetrics, setTargetMetrics] = useState<string>('');
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
// 定义侧边栏样式
const sidebarStyle = {
transform: isSidebarOpen ? 'translateX(0)' : 'translateX(-100%)',
transition: 'transform 0.3s ease-in-out',
width: '250px',
position: 'fixed',
top: 0,
left: 0,
height: '100%',
backgroundColor: '#f4f4f4',
zIndex: 1000,
};
// 其他函数和效果…
useEffect(() => {
(async () => {
const req = { perspectives: [] };
commonAjax
.axios({ loading: true })
.post('/api/digitalValueTreeInfo', req)
.then((res) => {
setDispElements(res.data.elements);
setElementsStyle(res.data.style);
setContentHeight(
window.innerHeight -
document.getElementsByClassName('main-header')[0].clientHeight -
document.getElementsByClassName('page-cover')[0].clientHeight -
document.getElementsByClassName('content-header')[0].clientHeight -
42,
);
setRefreshFlg(!refreshFlg);
});
})();
}, []);
// 省略其他相关的 useEffect 和函数定义...
return (
<div className="content-wrapper" id="digital-value-tree">
<section className="page-cover">
<h1>Digital value tree</h1>
</section>
<section className="content-header">
<div className="content-header-left">
<h1>Digital value tree</h1>
<div className="content-header-desc">
{/* 概要描述 */}
</div>
</div>
<div className="btn-right btn-lg">
<i className={`fa fa-solid fa-expand`} aria-hidden="true" onClick={toggleSidebar}></i>
</div>
</section>
<section className="content">
<div style={sidebarStyle} id="sidebar">
{/* 侧边栏内容 */}
<div style={{ padding: '20px' }}>
<h4>财务指标</h4>
{/* 其他财务指标相关内容 */}
<h4>模拟模式</h4>
<div className="form-check">
<input
type="checkbox"
id="simulationCheckBox"
className="form-check-input"
checked={simulationCheck}
onChange={(e) => setSimulationCheck(e.target.checked)}
/>
<label className="form-check-label" htmlFor="simulationCheckBox">
模拟模式
</label>
</div>
</div>
</div>
<div id="contentBody" style={{ marginLeft: isSidebarOpen ? '250px' : '0', transition: 'margin-left 0.3s ease-in-out' }}>
<div ref={graphContainer} id="graphContainer" style={{ height: contentHeight }} />
</div>
</section>
</div>
);
}
export default DigitalValueTree;
import cytoscape, { Collection, NodeCollection } from 'cytoscape';
import cxtmenu from 'cytoscape-cxtmenu';
import dagre from 'cytoscape-dagre';
import cyHtmlLabel from 'cytoscape-node-html-label';
import { useEffect, useRef, useState } from 'react';
import { commonAjax } from '../../../components/commonAjax';
cytoscape.use(dagre);
cytoscape.use(cxtmenu);
cytoscape.use(cyHtmlLabel);
function DigitalValueTree() {
const graphContainer = useRef<HTMLDivElement>(null);
const [dispElements, setDispElements] = useState<[]>([]);
const [elementsStyle, setElementsStyle] = useState<[]>([]);
const [contentHeight, setContentHeight] = useState<number | string>(0);
const [refreshFlg, setRefreshFlg] = useState<boolean>(true);
const [perspectiveCheck, setPerspectiveCheck] = useState<boolean>(true);
const [simulationCheck, setSimulationCheck] = useState<boolean>(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [targetMetrics, setTargetMetrics] = useState<string>('');
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
useEffect(() => {
(async () => {
const req = { perspectives: [] };
commonAjax
.axios({ loading: true })
.post('/api/digitalValueTreeInfo', req)
.then((res) => {
setDispElements(res.data.elements);
setElementsStyle(res.data.style);
setContentHeight(
window.innerHeight -
document.getElementsByClassName('main-header')[0].clientHeight -
document.getElementsByClassName('page-cover')[0].clientHeight -
document.getElementsByClassName('content-header')[0].clientHeight -
42,
);
setRefreshFlg(!refreshFlg);
});
})();
}, []);
return (
<div className="content-wrapper" id="digital-value-tree">
<section className="page-cover">
<h1>Digital value tree</h1>
</section>
<section className="content-header">
<div className="content-header-left">
<h1>Digital value tree</h1>
<div className="content-header-desc">
**************概要**************
</div>
</div>
</section>
<section className="content">
<div
style={{
width: isSidebarOpen ? '250px' : '0',
transition: 'width 0.3s ease-in-out',
overflow: 'hidden',
position: 'fixed',
right: 0,
top: 0,
bottom: 0,
backgroundColor: '#f4f4f4',
zIndex: 1000,
}}
>
{/* 侧边栏内容 */}
<div style={{ padding: '20px' }}>
<h4>财务指标</h4>
{/* 其他财务指标相关内容 */}
<h4>模拟模式</h4>
<div className="form-check">
<input
type="checkbox"
id="simulationCheckBox"
className="form-check-input"
checked={simulationCheck}
onChange={(e) => setSimulationCheck(e.target.checked)}
/>
<label className="form-check-label" htmlFor="simulationCheckBox">
模拟模式
</label>
</div>
</div>
</div>
<button
onClick={toggleSidebar}
style={{
position: 'fixed',
right: isSidebarOpen ? '250px' : '0',
top: '10px',
zIndex: 1001, // Ensure button is always clickable
transition: 'right 0.3s ease-in-out',
}}
>
{isSidebarOpen ? '关闭侧边栏' : '打开侧边栏'}
</button>
<div id="contentBody" style={{ paddingRight: isSidebarOpen ? '250px' : '0', transition: 'padding-right 0.3s ease-in-out' }}>
<div ref={graphContainer} id="graphContainer" style={{ height: contentHeight }} />
</div>
</section>
</div>
);
}
export default DigitalValueTree;
import cytoscape, { Collection, NodeCollection } from 'cytoscape';
import cxtmenu from 'cytoscape-cxtmenu';
import dagre from 'cytoscape-dagre';
// @ts-ignore
import cyHtmlLabel from 'cytoscape-node-html-label';
import { useEffect, useRef, useState } from 'react';
import { commonAjax } from '../../../components/commonAjax';
cytoscape.use(dagre);
cytoscape.use(cxtmenu);
cytoscape.use(cyHtmlLabel);
function digitalValueTree() {
const graphContainer = useRef<HTMLDivElement>(null);
const [dispElements, setDispElements] = useState<[]>([]);
const [elementsStyle, setElementsStyle] = useState<[]>([]);
const [contentHeight, setContentHeight] = useState<number | string>(0);
const [refreshFlg, setRefreshFlg] = useState<boolean>(true);
const [perspectiveCheck, setPerspectiveCheck] = useState<boolean>(true);
const [simulationCheck, setSimulationCheck] = useState<boolean>(false);
const [targetMetrics, setTargetMetrics] = useState<string>('');
const [isSidebarOpen,setIsSidebarOpen] = useState<boolean>(false);
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen)
let tappedBefore: null;
let tappedTimeout: string | number | NodeJS.Timeout | undefined;
// 視点フィルター変更時イベント
function changePerspective() {
const vals: string[] = [];
$('input[class="form-check-input"]:checked').each(function () {
vals.push($(this)[0].id);
});
const req = { perspectives: vals };
(async () => {
commonAjax
.axios({ loading: true })
.post('/api/digitalValueTreeInfo', req)
.then((res) => {
setDispElements(res.data.elements);
setElementsStyle(res.data.style);
setRefreshFlg(!refreshFlg);
});
})();
}
// シミュレーションモード変更時変更時イベント
function changeSimulationMode() {
const CheckBox = document.querySelector(
"input[type='checkbox']#simulationCheckBox",
) as HTMLInputElement;
setSimulationCheck(CheckBox.checked);
setRefreshFlg(!refreshFlg);
}
// 全画面モード選択時イベント
function fullscreen() {
document.getElementById('contentBody')!.requestFullscreen();
// setContentHeight(window.outerHeight);
// setContentHeight('100%');
// setRefreshFlg(!refreshFlg);
}
// フルスクリーン切替時イベント
function fullscreenchanged() {
if (document.fullscreenElement) {
// setContentHeight('100%');
setContentHeight(window.outerHeight);
setRefreshFlg(!refreshFlg);
// document.getElementById('contentBody')!.style.backgroundColor = 'white';
} else {
setContentHeight(
window.innerHeight -
document.getElementsByClassName('main-header')[0].clientHeight -
document.getElementsByClassName('page-cover')[0].clientHeight -
document.getElementsByClassName('content-header')[0].clientHeight -
42,
);
setRefreshFlg(!refreshFlg);
// document.getElementById('contentBody')!.style.backgroundColor =
// 'var(--content-bg-color)';
}
}
// フルスクリーン切替イベントリスナー
document.addEventListener('fullscreenchange', fullscreenchanged);
useEffect(() => {
(async () => {
const vals = [] as any;
const req = { perspectives: vals };
commonAjax
.axios({ loading: true })
.post('/api/digitalValueTreeInfo', req)
.then((res) => {
setDispElements(res.data.elements);
setElementsStyle(res.data.style);
setContentHeight(
window.innerHeight -
document.getElementsByClassName('main-header')[0].clientHeight -
document.getElementsByClassName('page-cover')[0].clientHeight -
document.getElementsByClassName('content-header')[0]
.clientHeight -
42,
);
setRefreshFlg(!refreshFlg);
});
})();
}, []);
useEffect(() => {
if (graphContainer.current) {
// cytoscape init
const cy = cytoscape({
wheelSensitivity: 0.1, // マウスホイール拡大縮小感度
container: graphContainer.current,
elements: dispElements,
style: elementsStyle,
});
// ノード幅動的設定
const labelWidth = (node: NodeCollection): number => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
context.font = ['style', 'weight', 'size', 'family']
.map((style) => node.style(`font-${style}`))
.join(' ');
const { width } = context.measureText(node.data('label'));
const padding = 20;
return width + padding;
};
// シュミレーションモード分岐
if (simulationCheck) {
/* *********************************
ここにシュミレーション処理追加!!!
********************************* */
// sample-ノードのラベルにHTMLを表示するための設定
// @ts-ignore
cy.nodeHtmlLabel(
[
{
query: 'node[group="metrics"]',
halign: 'center',
valign: 'center',
tpl: (
data: any,
) => `<table style="border: 2px solid rgb(140 140 140);">
<thead>
<tr>
<th scope="col" style="border: 1px solid rgb(160 160 160)">head1</th>
<th scope="col" style="border: 1px solid rgb(160 160 160)">head2</th>
</tr>
</thead>
<tbody>
<tr>
<th style="border: 1px solid rgb(160 160 160)">col1</th>
<td style="border: 1px solid rgb(160 160 160)">col2</td>
</tr>
</tbody>
</table>
`,
},
// {
// query: "node[id='metrics2']",
// halign: 'center',
// valign: 'center',
// tpl: (data: any) =>
// `<div style="background-color: red; border-radius:10px; padding:5; margin:0 0 45 130;">badge</div>`,
// },
// {
// query: "node[id='metrics3']",
// halign: 'center',
// valign: 'center',
// tpl: (data: any) => `<input id="sssss" style="margin:50 0 0 0;"/>`,
// },
],
// {
// enablePointerEvents: true,
// },
);
cy.nodes("not(node[group='metrics'])").forEach((node) => {
node.style('width', labelWidth(node));
});
cy.nodes('node[group="metrics"]').forEach((ele) => {
ele.style('width', '140');
ele.style('height', '150');
ele.style('text-valign', 'top');
ele.style('text-halign', 'center');
});
} else {
// cxtmenu
const metricsCxtmenu = {
selector: 'node[group="metrics"]',
menuRadius: 75,
activePadding: 8,
openMenuEvents: 'tap',
// openMenuEvents: 'cxttapstart',
// openMenuEvents: 'cxttapstart taphold',
outsideMenuCancel: 8,
menuItemTextSize: 100,
commands: (ele: Collection) => [
{
content: '詳細',
contentStyle: {},
enabled: ele.data('logical_name') !== null,
select(element: any) {
const openNewTab = (url: string) => {
const newWindow = window.open(url, '_blank');
if (newWindow) {
newWindow.opener = null;
}
};
const targetId = element.data('logical_name');
openNewTab(`/#/scm-metrics-details/${targetId}`);
// history.push(`/scm-metrics-details/${targetId}`);//新規タブで開かない場合
},
},
{
content: 'リレーションハイライト',
contentStyle: {},
enabled: true,
select(element: any) {
cy.elements().removeClass('unhighlighted'); // 既存のクラスをクリア
cy.elements().removeClass('highlighted'); // 既存のクラスをクリア
// element.outgoers().addClass('highlighted'); // 接続されているエッジとノードをハイライト
element.predecessors().addClass('highlighted'); // 上流エッジとノードをハイライト
element.successors().addClass('highlighted'); // 下流エッジとノードをハイライト
cy.elements()
// .not(element.outgoers())
.not(element.predecessors()) // 上流
.not(element.successors()) // 下流
.not(element) // 選択ノード
.not('node[group="hidden_item"]') // 選択ノード
.not('node[group="perspective"]') // 選択ノード
.addClass('unhighlighted'); // それ以外をグレーアウト
},
},
{
content: 'シミュレーション',
contentStyle: {},
enabled: false,
},
{
content: '参照データ',
contentStyle: {},
enabled: ele.data('logical_name') !== null,
select(element: any) {
setTargetMetrics(element.data('id'));
},
},
],
};
const otherCxtmenu = {
selector:
'node[group="financial_parent"],node[group="financial_child"],node[group="pl"],node[group="bs"],node[group="reform"],node[group="measure_category"],node[group="measure"]',
menuRadius: 75,
activePadding: 8,
openMenuEvents: 'tap',
// openMenuEvents: 'cxttapstart',
// openMenuEvents: 'cxttapstart taphold',
outsideMenuCancel: 8,
menuItemTextSize: 100,
commands: [
{
content: 'リレーションハイライト',
contentStyle: {},
enabled: true,
select(element: any) {
cy.elements().removeClass('unhighlighted'); // 既存のクラスをクリア
cy.elements().removeClass('highlighted'); // 既存のクラスをクリア
// element.outgoers().addClass('highlighted'); // 接続されているエッジとノードをハイライト
element.predecessors().addClass('highlighted'); // 上流エッジとノードをハイライト
element.successors().addClass('highlighted'); // 下流エッジとノードをハイライト
cy.elements()
// .not(element.outgoers())
.not(element.predecessors()) // 上流
.not(element.successors()) // 下流
.not(element) // 選択ノード
.not('node[group="hidden_item"]') // 選択ノード
.not('node[group="plBs"]') // 選択ノード
.not('node[group="perspective"]') // 選択ノード
.addClass('unhighlighted'); // それ以外をグレーアウト
},
},
],
};
cy.cxtmenu(metricsCxtmenu);
cy.cxtmenu(otherCxtmenu);
// canvasクリック時イベント
// cy.on('tap', () => {
// cy.elements().removeClass('highlighted unhighlighted'); // 選択解除時にクラスをクリア
// });
// ダブルクリックでハイライトクラスクリアver
cy.on('tap', (event) => {
// NOTE: older cytoscape's event has cyTarget rather than target.
const tappedNow = event.target;
if (tappedTimeout && tappedBefore) {
clearTimeout(tappedTimeout);
}
if (tappedBefore === tappedNow) {
tappedNow.trigger('doubleTap');
tappedBefore = null;
} else {
tappedTimeout = setTimeout(() => {
tappedBefore = null;
}, 300);
tappedBefore = tappedNow;
}
});
cy.on('doubleTap', () => {
cy.elements().removeClass('highlighted unhighlighted'); // 選択解除時にクラスをクリア
});
cy.nodes().forEach((node) => {
node.style('width', labelWidth(node));
});
}
const layout = {
name: 'dagre',
nodeSep: 50,
edgeSep: 0,
rankSep: 30,
// animate: true,
// padding: 10,
};
cy.layout(layout).run();
return () => {
cy.destroy();
};
}
}, [refreshFlg]);
useEffect(() => {
if (targetMetrics) {
// (async () => {
// const vals = [] as any;
// const req = { perspectives: vals };
// commonAjax
// .axios({ loading: true })
// .get(`/api/digitalValueTreeInfo/{targetMetrics}`)
// .then((res) => {
// setDispElements(res.data.elements);
// setElementsStyle(res.data.style);
// setContentHeight(
// window.innerHeight -
// document.getElementsByClassName('main-header')[0].clientHeight -
// document.getElementsByClassName('page-cover')[0].clientHeight -
// document.getElementsByClassName('content-header')[0]
// .clientHeight -
// 42,
// );
// setRefreshFlg(!refreshFlg);
// });
// })();
const parentDivEle = document.getElementById('apiInfoPanelContents');
const tmpParentDivEle = document.createElement('div');
const tmpChildDivEle = document.createElement('div');
const iEle = document.createElement('i');
tmpParentDivEle.id = targetMetrics;
tmpParentDivEle.className = 'apiInfoPanel';
iEle.className = 'fa fa-times-circle fa-lg float-right';
iEle.ariaHidden = 'true';
iEle.onclick = function () {
document.getElementById(tmpParentDivEle.id)?.remove();
};
tmpParentDivEle.appendChild(iEle);
tmpChildDivEle.textContent = `${targetMetrics}`;
tmpParentDivEle.appendChild(tmpChildDivEle);
setTargetMetrics('');
// parentDivEle?.appendChild(tmpParentDivEle);//最下部に追加するパターン
parentDivEle?.insertBefore(tmpParentDivEle, parentDivEle?.children[0]); // 最上部に追加するパターン
}
}, [targetMetrics]);
return (
<div className="content-wrapper" id="digital-value-tree">
<section className="page-cover">
<h1>Digital value tree</h1>
</section>
<section className="content-header">
<div className="content-header-left">
<h1>Digital value tree</h1>
<div className="content-header-desc">
**************概要**************
</div>
{/* <input type="button" onClick={fullscreen} value="全画面モード" /> */}
{/* <Link to="/metrics-network-tab" className="nav-link" target="_blank">
全画面モード
</Link> */}
</div>
<div className="btn-right btn-lg">
<i className={`fa fa-solid fa-expand`}
aria-hidden="true"
onClick={fullscreen}
>
</i>
</div>
</section>
<section className="content" style={{position:'relative'}}>
<div
id="contentBody"
style={{
backgroundColor: 'var(--content-bg-color)',
}}
>
<div
id="legendContens"
style={{
top: 40,
right: 10,
}}
>
<div>
<ul>
<li id="legend1">財務指標</li>
<li id="legend2">P/L</li>
<li id="legend3">B/S</li>
<li id="legend4">改革指標</li>
<li id="legend5">施策カテゴリー</li>
<li id="legend6">施策</li>
<li id="legend7">KPI例</li>
</ul>
</div>
</div>
{!isSidebarOpen && (
<i className="fas fa-bars btn-lg" onClick={toggleSidebar} style={{position:'absolute',right:'0',top:'10px',zIndex:1001,}}>
setting
</i>
)}
<div
style={{
width: isSidebarOpen ? '250px' : '0',
transition: 'width 0.3s ease-in-out',
overflow: 'hidden',
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
backgroundColor: '#f3f3f3',
zIndex: 1000,
}}
>
<div
id="perspectiveContents"
style={{
top: 50,
right: 10,
}}
>
<div className="form-check">
<input
type="checkbox"
id="scm"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label className="form-check-label" htmlFor="scm">
SCM(需給/調達)視点
</label>
</div>
<div className="form-check">
<input
type="checkbox"
id="logistics"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label className="form-check-label" htmlFor="logistics">
ロジスティックス視点
</label>
</div>
<div className="form-check">
<input
type="checkbox"
id="production_quality_assurance"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label
className="form-check-label"
htmlFor="production_quality_assurance"
>
生産・品質保証視点
</label>
</div>
</div>
<div
id="simulationContents"
style={{
top: 120,
right: 10,
}}
>
<div className="form-check">
<input
type="checkbox"
id="simulationCheckBox"
className="form-check-input"
defaultChecked={simulationCheck}
onChange={changeSimulationMode}
/>
<label className="form-check-label" htmlFor="simulationCheckBox">
シミュレーションモード
</label>
</div>
</div>
</div>
{isSidebarOpen && (
<i className="fas fa-bars btn-lg" onClick={toggleSidebar} style={{position:'absolute',right:'0',top:'10px',zIndex:1001,}}>
close
</i>
)}
<div id="apiInfoPanelContents" />
<div
ref={graphContainer}
id="graphContainer"
style={{
height: contentHeight,
}}
/>
</div>
</section>
</div>
);
}
export default digitalValueTree;
.form-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.form-switch .form-check-input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
.form-check-input:checked + .slider {
background-color: #2196F3;
}
.form-check-input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
.form-check-input:checked + .slider:before {
transform: translateX(26px);
}
<div className="form-switch">
<input
type="checkbox"
id="simulationSwitch"
className="form-check-input"
defaultChecked={simulationCheck}
onChange={changeSimulationMode}
/>
<label className="slider" htmlFor="simulationSwitch"></label>
</div>
import cytoscape, { Collection, NodeCollection } from 'cytoscape';
import cxtmenu from 'cytoscape-cxtmenu';
import dagre from 'cytoscape-dagre';
// @ts-ignore
import cyHtmlLabel from 'cytoscape-node-html-label';
import { useEffect, useRef, useState } from 'react';
import { commonAjax } from '../../../components/commonAjax';
cytoscape.use(dagre);
cytoscape.use(cxtmenu);
cytoscape.use(cyHtmlLabel);
function digitalValueTree() {
const graphContainer = useRef<HTMLDivElement>(null);
const [dispElements, setDispElements] = useState<[]>([]);
const [elementsStyle, setElementsStyle] = useState<[]>([]);
const [contentHeight, setContentHeight] = useState<number | string>(0);
const [refreshFlg, setRefreshFlg] = useState<boolean>(true);
const [perspectiveCheck, setPerspectiveCheck] = useState<boolean>(true);
const [simulationCheck, setSimulationCheck] = useState<boolean>(false);
const [simulationSwitch, setSimulationSwitch] = useState<boolean>(false);
const [targetMetrics, setTargetMetrics] = useState<string>('');
const [isSidebarOpen,setIsSidebarOpen] = useState<boolean>(false);
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen)
let tappedBefore: null;
let tappedTimeout: string | number | NodeJS.Timeout | undefined;
// 視点フィルター変更時イベント
function changePerspective() {
const vals: string[] = [];
$('input[class="form-check-input"]:checked').each(function () {
vals.push($(this)[0].id);
});
const req = { perspectives: vals };
(async () => {
commonAjax
.axios({ loading: true })
.post('/api/digitalValueTreeInfo', req)
.then((res) => {
setDispElements(res.data.elements);
setElementsStyle(res.data.style);
setRefreshFlg(!refreshFlg);
});
})();
}
// シミュレーションモード変更時変更時イベント
function changeSimulationMode() {
const CheckBox = document.querySelector(
"input[type='checkbox']#simulationSwitch",
) as HTMLInputElement;
setSimulationCheck(CheckBox.checked);
setRefreshFlg(!refreshFlg);
}
// 全画面モード選択時イベント
function fullscreen() {
document.getElementById('contentBody')!.requestFullscreen();
// setContentHeight(window.outerHeight);
// setContentHeight('100%');
// setRefreshFlg(!refreshFlg);
}
// フルスクリーン切替時イベント
function fullscreenchanged() {
if (document.fullscreenElement) {
// setContentHeight('100%');
setContentHeight(window.innerHeight);
setRefreshFlg(!refreshFlg);
// document.getElementById('contentBody')!.style.backgroundColor = 'white';
} else {
setContentHeight(
window.innerHeight -
document.getElementsByClassName('main-header')[0].clientHeight -
document.getElementsByClassName('page-cover')[0].clientHeight -
document.getElementsByClassName('content-header')[0].clientHeight -
42,
);
setRefreshFlg(!refreshFlg);
// document.getElementById('contentBody')!.style.backgroundColor =
// 'var(--content-bg-color)';
}
}
// フルスクリーン切替イベントリスナー
document.addEventListener('fullscreenchange', fullscreenchanged);
useEffect(() => {
(async () => {
const vals = [] as any;
const req = { perspectives: vals };
commonAjax
.axios({ loading: true })
.post('/api/digitalValueTreeInfo', req)
.then((res) => {
setDispElements(res.data.elements);
setElementsStyle(res.data.style);
setContentHeight(
window.innerHeight -
document.getElementsByClassName('main-header')[0].clientHeight -
document.getElementsByClassName('page-cover')[0].clientHeight -
document.getElementsByClassName('content-header')[0]
.clientHeight -
42,
);
setRefreshFlg(!refreshFlg);
});
})();
}, []);
useEffect(() => {
if (graphContainer.current) {
// cytoscape init
const cy = cytoscape({
wheelSensitivity: 0.1, // マウスホイール拡大縮小感度
container: graphContainer.current,
elements: dispElements,
style: elementsStyle,
});
// ノード幅動的設定
const labelWidth = (node: NodeCollection): number => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
context.font = ['style', 'weight', 'size', 'family']
.map((style) => node.style(`font-${style}`))
.join(' ');
const { width } = context.measureText(node.data('label'));
const padding = 20;
return width + padding;
};
// シュミレーションモード分岐
if (simulationCheck) {
/* *********************************
ここにシュミレーション処理追加!!!
********************************* */
// sample-ノードのラベルにHTMLを表示するための設定
// @ts-ignore
cy.nodeHtmlLabel(
[
{
query: 'node[group="metrics"]',
halign: 'center',
valign: 'center',
tpl: (
data: any,
) => `<table style="border: 2px solid rgb(140 140 140);">
<thead>
<tr>
<th scope="col" style="border: 1px solid rgb(160 160 160)">head1</th>
<th scope="col" style="border: 1px solid rgb(160 160 160)">head2</th>
</tr>
</thead>
<tbody>
<tr>
<th style="border: 1px solid rgb(160 160 160)">col1</th>
<td style="border: 1px solid rgb(160 160 160)">col2</td>
</tr>
</tbody>
</table>
`,
},
// {
// query: "node[id='metrics2']",
// halign: 'center',
// valign: 'center',
// tpl: (data: any) =>
// `<div style="background-color: red; border-radius:10px; padding:5; margin:0 0 45 130;">badge</div>`,
// },
// {
// query: "node[id='metrics3']",
// halign: 'center',
// valign: 'center',
// tpl: (data: any) => `<input id="sssss" style="margin:50 0 0 0;"/>`,
// },
],
// {
// enablePointerEvents: true,
// },
);
cy.nodes("not(node[group='metrics'])").forEach((node) => {
node.style('width', labelWidth(node));
});
cy.nodes('node[group="metrics"]').forEach((ele) => {
ele.style('width', '140');
ele.style('height', '150');
ele.style('text-valign', 'top');
ele.style('text-halign', 'center');
});
} else {
// cxtmenu
const metricsCxtmenu = {
selector: 'node[group="metrics"]',
menuRadius: 75,
activePadding: 8,
openMenuEvents: 'tap',
// openMenuEvents: 'cxttapstart',
// openMenuEvents: 'cxttapstart taphold',
outsideMenuCancel: 8,
menuItemTextSize: 100,
commands: (ele: Collection) => [
{
content: '詳細',
contentStyle: {},
enabled: ele.data('logical_name') !== null,
select(element: any) {
const openNewTab = (url: string) => {
const newWindow = window.open(url, '_blank');
if (newWindow) {
newWindow.opener = null;
}
};
const targetId = element.data('logical_name');
openNewTab(`/#/scm-metrics-details/${targetId}`);
// history.push(`/scm-metrics-details/${targetId}`);//新規タブで開かない場合
},
},
{
content: 'リレーションハイライト',
contentStyle: {},
enabled: true,
select(element: any) {
cy.elements().removeClass('unhighlighted'); // 既存のクラスをクリア
cy.elements().removeClass('highlighted'); // 既存のクラスをクリア
// element.outgoers().addClass('highlighted'); // 接続されているエッジとノードをハイライト
element.predecessors().addClass('highlighted'); // 上流エッジとノードをハイライト
element.successors().addClass('highlighted'); // 下流エッジとノードをハイライト
cy.elements()
// .not(element.outgoers())
.not(element.predecessors()) // 上流
.not(element.successors()) // 下流
.not(element) // 選択ノード
.not('node[group="hidden_item"]') // 選択ノード
.not('node[group="perspective"]') // 選択ノード
.addClass('unhighlighted'); // それ以外をグレーアウト
},
},
{
content: 'シミュレーション',
contentStyle: {},
enabled: false,
},
{
content: '参照データ',
contentStyle: {},
enabled: ele.data('logical_name') !== null,
select(element: any) {
setTargetMetrics(element.data('id'));
},
},
],
};
const otherCxtmenu = {
selector:
'node[group="financial_parent"],node[group="financial_child"],node[group="pl"],node[group="bs"],node[group="reform"],node[group="measure_category"],node[group="measure"]',
menuRadius: 75,
activePadding: 8,
openMenuEvents: 'tap',
// openMenuEvents: 'cxttapstart',
// openMenuEvents: 'cxttapstart taphold',
outsideMenuCancel: 8,
menuItemTextSize: 100,
commands: [
{
content: 'リレーションハイライト',
contentStyle: {},
enabled: true,
select(element: any) {
cy.elements().removeClass('unhighlighted'); // 既存のクラスをクリア
cy.elements().removeClass('highlighted'); // 既存のクラスをクリア
// element.outgoers().addClass('highlighted'); // 接続されているエッジとノードをハイライト
element.predecessors().addClass('highlighted'); // 上流エッジとノードをハイライト
element.successors().addClass('highlighted'); // 下流エッジとノードをハイライト
cy.elements()
// .not(element.outgoers())
.not(element.predecessors()) // 上流
.not(element.successors()) // 下流
.not(element) // 選択ノード
.not('node[group="hidden_item"]') // 選択ノード
.not('node[group="plBs"]') // 選択ノード
.not('node[group="perspective"]') // 選択ノード
.addClass('unhighlighted'); // それ以外をグレーアウト
},
},
],
};
cy.cxtmenu(metricsCxtmenu);
cy.cxtmenu(otherCxtmenu);
// canvasクリック時イベント
// cy.on('tap', () => {
// cy.elements().removeClass('highlighted unhighlighted'); // 選択解除時にクラスをクリア
// });
// ダブルクリックでハイライトクラスクリアver
cy.on('tap', (event) => {
// NOTE: older cytoscape's event has cyTarget rather than target.
const tappedNow = event.target;
if (tappedTimeout && tappedBefore) {
clearTimeout(tappedTimeout);
}
if (tappedBefore === tappedNow) {
tappedNow.trigger('doubleTap');
tappedBefore = null;
} else {
tappedTimeout = setTimeout(() => {
tappedBefore = null;
}, 300);
tappedBefore = tappedNow;
}
});
cy.on('doubleTap', () => {
cy.elements().removeClass('highlighted unhighlighted'); // 選択解除時にクラスをクリア
});
cy.nodes().forEach((node) => {
node.style('width', labelWidth(node));
});
}
const layout = {
name: 'dagre',
nodeSep: 50,
edgeSep: 0,
rankSep: 30,
// animate: true,
// padding: 10,
};
cy.layout(layout).run();
return () => {
cy.destroy();
};
}
}, [refreshFlg]);
useEffect(() => {
if (targetMetrics) {
// (async () => {
// const vals = [] as any;
// const req = { perspectives: vals };
// commonAjax
// .axios({ loading: true })
// .get(`/api/digitalValueTreeInfo/{targetMetrics}`)
// .then((res) => {
// setDispElements(res.data.elements);
// setElementsStyle(res.data.style);
// setContentHeight(
// window.innerHeight -
// document.getElementsByClassName('main-header')[0].clientHeight -
// document.getElementsByClassName('page-cover')[0].clientHeight -
// document.getElementsByClassName('content-header')[0]
// .clientHeight -
// 42,
// );
// setRefreshFlg(!refreshFlg);
// });
// })();
const parentDivEle = document.getElementById('apiInfoPanelContents');
const tmpParentDivEle = document.createElement('div');
const tmpChildDivEle = document.createElement('div');
const iEle = document.createElement('i');
tmpParentDivEle.id = targetMetrics;
tmpParentDivEle.className = 'apiInfoPanel';
iEle.className = 'fa fa-times-circle fa-lg float-right';
iEle.ariaHidden = 'true';
iEle.onclick = function () {
document.getElementById(tmpParentDivEle.id)?.remove();
};
tmpParentDivEle.appendChild(iEle);
tmpChildDivEle.textContent = `${targetMetrics}`;
tmpParentDivEle.appendChild(tmpChildDivEle);
setTargetMetrics('');
// parentDivEle?.appendChild(tmpParentDivEle);//最下部に追加するパターン
parentDivEle?.insertBefore(tmpParentDivEle, parentDivEle?.children[0]); // 最上部に追加するパターン
}
}, [targetMetrics]);
return (
<div className="content-wrapper" id="digital-value-tree">
<section className="page-cover">
<h1>Digital value tree</h1>
</section>
<section className="content-header">
<div className="content-header-left">
<h1>Digital value tree</h1>
<div className="content-header-desc">
**************概要**************
</div>
{/* <input type="button" onClick={fullscreen} value="全画面モード" /> */}
{/* <Link to="/metrics-network-tab" className="nav-link" target="_blank">
全画面モード
</Link> */}
</div>
<div className="btn-right btn-lg">
<i className={`fa fa-solid fa-expand`}
aria-hidden="true"
onClick={fullscreen}
>
</i>
</div>
</section>
<section className="content" style={{position:'relative'}}>
<div
id="contentBody"
style={{
backgroundColor: 'var(--content-bg-color)',
}}
>
<div
id="legendContens"
style={{
top: 60,
right: 10,
}}
>
<div>
<ul>
<li id="legend1">財務指標</li>
<li id="legend2">P/L</li>
<li id="legend3">B/S</li>
<li id="legend4">改革指標</li>
<li id="legend5">施策カテゴリー</li>
<li id="legend6">施策</li>
<li id="legend7">KPI例</li>
</ul>
</div>
</div>
{!isSidebarOpen && (
<i className="fas fa-bars btn-lg" onClick={toggleSidebar} style={{position:'absolute',right:'0',top:'10px',zIndex:1001,}}>
</i>
)}
<div
style={{
width: isSidebarOpen ? '250px' : '0',
transition: 'width 0.3s ease-in-out',
overflow: 'hidden',
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
backgroundColor: '#f6f6f6',
zIndex: 1000,
borderRadius:'10px'
}}
>
{isSidebarOpen && (
<i className="fas fa-bars btn-lg" onClick={toggleSidebar} style={{position:'absolute',right:'0',top:'10px',zIndex:1001,}}>
</i>
)}
<div
id="perspectiveContents"
style={{
top: 50,
right: 10,
}}
>
視点フィルター
<div className="form-check">
<input
type="checkbox"
id="scm"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label className="form-check-label" htmlFor="scm">
SCM(需給/調達)視点
</label>
</div>
<div className="form-check">
<input
type="checkbox"
id="logistics"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label className="form-check-label" htmlFor="logistics">
ロジスティックス視点
</label>
</div>
<div className="form-check">
<input
type="checkbox"
id="production_quality_assurance"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label
className="form-check-label"
htmlFor="production_quality_assurance"
>
生産・品質保証視点
</label>
</div>
</div>
<div
id="simulationContents"
style={{
top: 200,
right: 10,
}}
>
<div className="form-switch">
<input
type="checkbox"
id="simulationSwitch"
className="form-check-input"
defaultChecked={simulationCheck}
onChange={changeSimulationMode}
/>
<label className="slider" htmlFor="simulationSwitch">シミュレーションモード</label>
</div>
</div>
</div>
<div id="apiInfoPanelContents" />
<div
ref={graphContainer}
id="graphContainer"
style={{
height: contentHeight,
}}
/>
</div>
</section>
</div>
);
}
export default digitalValueTree;
#digital-value-tree #contentBody {
position: relative;
}
#digital-value-tree #graphContainer {
width: 100%;
}
/*視点選択checkbox*/
#digital-value-tree #perspectiveContents{
z-index: 1;
position: absolute;
right: 0;
top: 0;
padding: 3px;
background-color: white;
border: 2px solid darkgray;
border-radius: 10px;
/* box-shadow: 5px 5px 20px #00000028; */
}
#digital-value-tree #perspectiveContents .form-check {
padding:5px;
}
/*シミュレーションモードcheckbox*/
#digital-value-tree #simulationContents{
width: 200px;
z-index: 1;
position: absolute;
right: 0;
top: 0;
padding: 3px;
background-color: white;
border: 2px solid darkgray;
border-radius: 10px;
/* box-shadow: 5px 5px 20px #00000028; */
}
#digital-value-tree #simulationContents .form-check {
padding:5px;
}
/*凡例スタイル*/
#digital-value-tree #legendContens{
z-index: 1;
position: absolute;
right: 0;
top: 0;
padding: 3px;
background-color: white;
border: 2px solid darkgray;
border-radius: 10px;
/* box-shadow: 5px 5px 20px #00000028; */
}
#digital-value-tree #legendContens li {
list-style-type: none;
}
#digital-value-tree #legendContens ul {
margin:0;
padding:0;
}
#digital-value-tree #legendContens li:before {
content: "";
display: inline-block;
height: 15px;
width: 50px;
margin-right: 5px;
opacity:0.3;
border-radius:5px;
}
#digital-value-tree #legendContens #legend1:before {
background-color: fuchsia;
}
#digital-value-tree #legendContens #legend2:before {
background-color: teal;
}
#digital-value-tree #legendContens #legend3:before {
background-color: yellow;
}
#digital-value-tree #legendContens #legend4:before {
background-color: red;
}
#digital-value-tree #legendContens #legend5:before {
background-color: blue;
}
#digital-value-tree #legendContens #legend6:before {
background-color: purple;
}
#digital-value-tree #legendContens #legend7:before {
background-color: gray;
}
/*INPUT情報パネル*/
#digital-value-tree #apiInfoPanelContents{
z-index: 2;
position: absolute;
right: 10px;
top: 10px;
/* box-shadow: 5px 5px 20px #00000028; */
}
#digital-value-tree .apiInfoPanel{
padding: 3px;
background-color: pink;
border: 2px solid darkgray;
border-radius: 10px;
}
/* アイコン fontawesome */
#digital-value-tree .fa{
/* color: #444; */
opacity: 0.3;
}
#digital-value-tree .fa:hover{
/* color: #444; */
opacity: 0.7;
}
#digital-value-tree .form-switch {
position: relative;
display: inline-block;
width: 50px;
height: 34px;
}
#digital-value-tree .form-switch .form-check-input {
opacity: 0;
width: 0;
height: 0;
}
#digital-value-tree .slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
#digital-value-tree .slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
#digital-value-tree .form-check-input:checked + .slider {
background-color: #2196F3;
}
#digital-value-tree .form-check-input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
#digital-value-tree .form-check-input:checked + .slider:before {
transform: translateX(26px);
}
import cytoscape, { Collection, NodeCollection } from 'cytoscape';
import cxtmenu from 'cytoscape-cxtmenu';
import dagre from 'cytoscape-dagre';
// @ts-ignore
import cyHtmlLabel from 'cytoscape-node-html-label';
import { useEffect, useRef, useState } from 'react';
import { commonAjax } from '../../../components/commonAjax';
cytoscape.use(dagre);
cytoscape.use(cxtmenu);
cytoscape.use(cyHtmlLabel);
function digitalValueTree() {
const graphContainer = useRef<HTMLDivElement>(null);
const [dispElements, setDispElements] = useState<[]>([]);
const [elementsStyle, setElementsStyle] = useState<[]>([]);
const [contentHeight, setContentHeight] = useState<number | string>(0);
const [refreshFlg, setRefreshFlg] = useState<boolean>(true);
const [perspectiveCheck, setPerspectiveCheck] = useState<boolean>(true);
const [simulationCheck, setSimulationCheck] = useState<boolean>(false);
const [simulationSwitch, setSimulationSwitch] = useState<boolean>(false);
const [targetMetrics, setTargetMetrics] = useState<string>('');
const [isSidebarOpen,setIsSidebarOpen] = useState<boolean>(false);
const [menuOpen,setMenuOpen] = useState<boolean>(false);
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen)
let tappedBefore: null;
let tappedTimeout: string | number | NodeJS.Timeout | undefined;
// 視点フィルター変更時イベント
function changePerspective() {
const vals: string[] = [];
$('input[class="form-check-input"]:checked').each(function () {
vals.push($(this)[0].id);
});
const req = { perspectives: vals };
(async () => {
commonAjax
.axios({ loading: true })
.post('/api/digitalValueTreeInfo', req)
.then((res) => {
setDispElements(res.data.elements);
setElementsStyle(res.data.style);
setRefreshFlg(!refreshFlg);
});
})();
}
// シミュレーションモード変更時変更時イベント
function changeSimulationMode() {
const CheckBox = document.querySelector(
"input[type='checkbox']#simulationSwitch",
) as HTMLInputElement;
setSimulationCheck(CheckBox.checked);
setRefreshFlg(!refreshFlg);
}
// 全画面モード選択時イベント
function fullscreen() {
document.getElementById('contentBody')!.requestFullscreen();
// setContentHeight(window.outerHeight);
// setContentHeight('100%');
// setRefreshFlg(!refreshFlg);
}
// フルスクリーン切替時イベント
function fullscreenchanged() {
if (document.fullscreenElement) {
// setContentHeight('100%');
setContentHeight(window.innerHeight);
setRefreshFlg(!refreshFlg);
// document.getElementById('contentBody')!.style.backgroundColor = 'white';
} else {
setContentHeight(
window.innerHeight -
document.getElementsByClassName('main-header')[0].clientHeight -
document.getElementsByClassName('page-cover')[0].clientHeight -
document.getElementsByClassName('content-header')[0].clientHeight -
42,
);
setRefreshFlg(!refreshFlg);
// document.getElementById('contentBody')!.style.backgroundColor =
// 'var(--content-bg-color)';
}
}
// フルスクリーン切替イベントリスナー
document.addEventListener('fullscreenchange', fullscreenchanged);
useEffect(() => {
(async () => {
const vals = [] as any;
const req = { perspectives: vals };
commonAjax
.axios({ loading: true })
.post('/api/digitalValueTreeInfo', req)
.then((res) => {
setDispElements(res.data.elements);
setElementsStyle(res.data.style);
setContentHeight(
window.innerHeight -
document.getElementsByClassName('main-header')[0].clientHeight -
document.getElementsByClassName('page-cover')[0].clientHeight -
document.getElementsByClassName('content-header')[0]
.clientHeight -
42,
);
setRefreshFlg(!refreshFlg);
});
})();
}, []);
useEffect(() => {
if (graphContainer.current) {
// cytoscape init
const cy = cytoscape({
wheelSensitivity: 0.1, // マウスホイール拡大縮小感度
container: graphContainer.current,
elements: dispElements,
style: elementsStyle,
});
// ノード幅動的設定
const labelWidth = (node: NodeCollection): number => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
context.font = ['style', 'weight', 'size', 'family']
.map((style) => node.style(`font-${style}`))
.join(' ');
const { width } = context.measureText(node.data('label'));
const padding = 20;
return width + padding;
};
// シュミレーションモード分岐
if (simulationCheck) {
/* *********************************
ここにシュミレーション処理追加!!!
********************************* */
// sample-ノードのラベルにHTMLを表示するための設定
// @ts-ignore
cy.nodeHtmlLabel(
[
{
query: 'node[group="metrics"]',
halign: 'center',
valign: 'center',
tpl: (
data: any,
) => `<table style="border: 2px solid rgb(140 140 140);">
<thead>
<tr>
<th scope="col" style="border: 1px solid rgb(160 160 160)">head1</th>
<th scope="col" style="border: 1px solid rgb(160 160 160)">head2</th>
</tr>
</thead>
<tbody>
<tr>
<th style="border: 1px solid rgb(160 160 160)">col1</th>
<td style="border: 1px solid rgb(160 160 160)">col2</td>
</tr>
</tbody>
</table>
`,
},
// {
// query: "node[id='metrics2']",
// halign: 'center',
// valign: 'center',
// tpl: (data: any) =>
// `<div style="background-color: red; border-radius:10px; padding:5; margin:0 0 45 130;">badge</div>`,
// },
// {
// query: "node[id='metrics3']",
// halign: 'center',
// valign: 'center',
// tpl: (data: any) => `<input id="sssss" style="margin:50 0 0 0;"/>`,
// },
],
// {
// enablePointerEvents: true,
// },
);
cy.nodes("not(node[group='metrics'])").forEach((node) => {
node.style('width', labelWidth(node));
});
cy.nodes('node[group="metrics"]').forEach((ele) => {
ele.style('width', '140');
ele.style('height', '150');
ele.style('text-valign', 'top');
ele.style('text-halign', 'center');
});
} else {
// cxtmenu
const metricsCxtmenu = {
selector: 'node[group="metrics"]',
menuRadius: 75,
activePadding: 8,
openMenuEvents: 'tap',
// openMenuEvents: 'cxttapstart',
// openMenuEvents: 'cxttapstart taphold',
outsideMenuCancel: 8,
menuItemTextSize: 100,
commands: (ele: Collection) => [
{
content: '詳細',
contentStyle: {},
enabled: ele.data('logical_name') !== null,
select(element: any) {
const openNewTab = (url: string) => {
const newWindow = window.open(url, '_blank');
if (newWindow) {
newWindow.opener = null;
}
};
const targetId = element.data('logical_name');
openNewTab(`/#/scm-metrics-details/${targetId}`);
// history.push(`/scm-metrics-details/${targetId}`);//新規タブで開かない場合
},
},
{
content: 'リレーションハイライト',
contentStyle: {},
enabled: true,
select(element: any) {
cy.elements().removeClass('unhighlighted'); // 既存のクラスをクリア
cy.elements().removeClass('highlighted'); // 既存のクラスをクリア
// element.outgoers().addClass('highlighted'); // 接続されているエッジとノードをハイライト
element.predecessors().addClass('highlighted'); // 上流エッジとノードをハイライト
element.successors().addClass('highlighted'); // 下流エッジとノードをハイライト
cy.elements()
// .not(element.outgoers())
.not(element.predecessors()) // 上流
.not(element.successors()) // 下流
.not(element) // 選択ノード
.not('node[group="hidden_item"]') // 選択ノード
.not('node[group="perspective"]') // 選択ノード
.addClass('unhighlighted'); // それ以外をグレーアウト
},
},
{
content: 'シミュレーション',
contentStyle: {},
enabled: false,
},
{
content: '参照データ',
contentStyle: {},
enabled: ele.data('logical_name') !== null,
select(element: any) {
setTargetMetrics(element.data('id'));
},
},
],
};
const otherCxtmenu = {
selector:
'node[group="financial_parent"],node[group="financial_child"],node[group="pl"],node[group="bs"],node[group="reform"],node[group="measure_category"],node[group="measure"]',
menuRadius: 75,
activePadding: 8,
openMenuEvents: 'tap',
// openMenuEvents: 'cxttapstart',
// openMenuEvents: 'cxttapstart taphold',
outsideMenuCancel: 8,
menuItemTextSize: 100,
commands: [
{
content: 'リレーションハイライト',
contentStyle: {},
enabled: true,
select(element: any) {
cy.elements().removeClass('unhighlighted'); // 既存のクラスをクリア
cy.elements().removeClass('highlighted'); // 既存のクラスをクリア
// element.outgoers().addClass('highlighted'); // 接続されているエッジとノードをハイライト
element.predecessors().addClass('highlighted'); // 上流エッジとノードをハイライト
element.successors().addClass('highlighted'); // 下流エッジとノードをハイライト
cy.elements()
// .not(element.outgoers())
.not(element.predecessors()) // 上流
.not(element.successors()) // 下流
.not(element) // 選択ノード
.not('node[group="hidden_item"]') // 選択ノード
.not('node[group="plBs"]') // 選択ノード
.not('node[group="perspective"]') // 選択ノード
.addClass('unhighlighted'); // それ以外をグレーアウト
},
},
],
};
cy.cxtmenu(metricsCxtmenu);
cy.cxtmenu(otherCxtmenu);
// canvasクリック時イベント
// cy.on('tap', () => {
// cy.elements().removeClass('highlighted unhighlighted'); // 選択解除時にクラスをクリア
// });
// ダブルクリックでハイライトクラスクリアver
cy.on('tap', (event) => {
// NOTE: older cytoscape's event has cyTarget rather than target.
const tappedNow = event.target;
if (tappedTimeout && tappedBefore) {
clearTimeout(tappedTimeout);
}
if (tappedBefore === tappedNow) {
tappedNow.trigger('doubleTap');
tappedBefore = null;
} else {
tappedTimeout = setTimeout(() => {
tappedBefore = null;
}, 300);
tappedBefore = tappedNow;
}
});
cy.on('doubleTap', () => {
cy.elements().removeClass('highlighted unhighlighted'); // 選択解除時にクラスをクリア
});
cy.nodes().forEach((node) => {
node.style('width', labelWidth(node));
});
}
const layout = {
name: 'dagre',
nodeSep: 50,
edgeSep: 0,
rankSep: 30,
// animate: true,
// padding: 10,
};
cy.layout(layout).run();
return () => {
cy.destroy();
};
}
}, [refreshFlg]);
useEffect(() => {
if (targetMetrics) {
// (async () => {
// const vals = [] as any;
// const req = { perspectives: vals };
// commonAjax
// .axios({ loading: true })
// .get(`/api/digitalValueTreeInfo/{targetMetrics}`)
// .then((res) => {
// setDispElements(res.data.elements);
// setElementsStyle(res.data.style);
// setContentHeight(
// window.innerHeight -
// document.getElementsByClassName('main-header')[0].clientHeight -
// document.getElementsByClassName('page-cover')[0].clientHeight -
// document.getElementsByClassName('content-header')[0]
// .clientHeight -
// 42,
// );
// setRefreshFlg(!refreshFlg);
// });
// })();
const parentDivEle = document.getElementById('apiInfoPanelContents');
const tmpParentDivEle = document.createElement('div');
const tmpChildDivEle = document.createElement('div');
const iEle = document.createElement('i');
tmpParentDivEle.id = targetMetrics;
tmpParentDivEle.className = 'apiInfoPanel';
iEle.className = 'fa fa-times-circle fa-lg float-right';
iEle.ariaHidden = 'true';
iEle.onclick = function () {
document.getElementById(tmpParentDivEle.id)?.remove();
};
tmpParentDivEle.appendChild(iEle);
tmpChildDivEle.textContent = `${targetMetrics}`;
tmpParentDivEle.appendChild(tmpChildDivEle);
setTargetMetrics('');
// parentDivEle?.appendChild(tmpParentDivEle);//最下部に追加するパターン
parentDivEle?.insertBefore(tmpParentDivEle, parentDivEle?.children[0]); // 最上部に追加するパターン
}
}, [targetMetrics]);
return (
<div className="content-wrapper" id="digital-value-tree">
<section className="page-cover">
<h1>Digital value tree</h1>
</section>
<section className="content-header">
<div className="content-header-left">
<h1>Digital value tree</h1>
<div className="content-header-desc">
**************概要**************
</div>
</div>
<div className="btn-right btn-lg" onClick={fullscreen}>
<i className={`fa fa-solid fa-expand`}
aria-hidden="true"
>
</i>
<span className="tooltip">
{'全画面モード'}
</span>
</div>
</section>
<section className="content" style={{position:'relative'}}>
<div
id="contentBody"
style={{
backgroundColor: 'var(--content-bg-color)',
}}
>
<div
id="legendContens"
style={{
top: 30,
right: 10,
}}
>
<div>
<ul>
<li id="legend1">財務指標</li>
<li id="legend2">P/L</li>
<li id="legend3">B/S</li>
<li id="legend4">改革指標</li>
<li id="legend5">施策カテゴリー</li>
<li id="legend6">施策</li>
<li id="legend7">KPI例</li>
</ul>
</div>
</div>
{!isSidebarOpen && (
<div className="btn-left btn-lg" onClick={toggleSidebar}>
<i className={`fas fa-cog btn-lg`}
style={{position:'absolute',right:'0',bottom:'0px',zIndex:1002,}}
>
</i>
<span className="tooltip">
{'設定'}
</span>
</div>
// {/* <i className="fas fa-cog btn-lg" onClick={toggleSidebar} style={{position:'absolute',right:'0',bottom:'0px',zIndex:1001,}}>
// </i>
// <span className="tooltip">
// {'設定'}
// </span> */}
)}
<div
style={{
width: isSidebarOpen ? '250px' : '0',
transition: 'width 0.3s ease-in-out',
overflow: 'hidden',
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
backgroundColor: '#f6f6f6',
zIndex: 1000,
borderRadius:'10px',
}}
>
{isSidebarOpen && (
<i className="fas fa-times btn-lg" onClick={toggleSidebar} style={{position:'absolute',right:'0',top:'0px',zIndex:1001,}}>
</i>
)}
<div
id="perspectiveContents"
style={{
top: 50,
right: 5,
}}
>
視点フィルター
<div className="form-check">
<input
type="checkbox"
id="scm"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label className="form-check-label" htmlFor="scm">
SCM(需給/調達)視点
</label>
</div>
<div className="form-check">
<input
type="checkbox"
id="logistics"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label className="form-check-label" htmlFor="logistics">
ロジスティックス視点
</label>
</div>
<div className="form-check">
<input
type="checkbox"
id="production_quality_assurance"
className="form-check-input"
defaultChecked={perspectiveCheck}
onChange={changePerspective}
/>
<label
className="form-check-label"
htmlFor="production_quality_assurance"
>
生産・品質保証視点
</label>
</div>
</div>
<div
id="simulationContents"
style={{
top: 200,
right: 10,
}}
>
<span className='swith-label'>シミュレーションモード</span>
<div className="form-switch">
<input
type="checkbox"
id="simulationSwitch"
className="form-check-input"
defaultChecked={simulationCheck}
onChange={changeSimulationMode}
/>
<label className="slider" htmlFor="simulationSwitch"></label>
</div>
</div>
</div>
<div id="apiInfoPanelContents" />
<div
ref={graphContainer}
id="graphContainer"
style={{
height: contentHeight,
}}
/>
</div>
</section>
</div>
);
}
export default digitalValueTree;
/* ツールチップ */
#digital-value-tree .btn-right { /* 補足説明するテキストのスタイル */
position: relative;
display: inline-block;
cursor: pointer;
}
#digital-value-tree .tooltip { /* ツールチップのスタイル */
width: 100px; /* 横幅 */
position: absolute;
left: 50%;
bottom: 80%; /* Y軸の位置 */
transform: translateX(-50%);
margin-bottom: 8px; /* テキストとの距離 */
padding: 8px;
border-radius: 10px; /* 角の丸み */
background-color: #666;
font-size: 0.6em;
color: #fff;
text-align: center;
visibility: hidden; /* ツールチップを非表示に */
opacity: 0; /* 不透明度を0%に */
z-index: 2;
transition: 0.5s all; /* マウスオーバー時のアニメーション速度 */
}
#digital-value-tree .btn-right:hover .tooltip { /* マウスオーバー時のスタイル */
visibility: visible; /* ツールチップを表示 */
opacity: 1; /* 不透明度を100%に */
}
/* ツールチップ */
#digital-value-tree .btn-right { /* 補足説明するテキストのスタイル */
display: inline-block;
cursor: pointer;
}
#digital-value-tree .tooltip { /* ツールチップのスタイル */
width: 100px; /* 横幅 */
position: absolute;
left: 50%;
bottom: 80%; /* Y軸の位置 */
transform: translateX(-50%);
margin-bottom: 8px; /* テキストとの距離 */
padding: 8px;
border-radius: 10px; /* 角の丸み */
background-color: #666;
font-size: 0.6em;
color: #fff;
text-align: center;
visibility: hidden; /* ツールチップを非表示に */
opacity: 0; /* 不透明度を0%に */
z-index: 2000;
transition: 0.5s all; /* マウスオーバー時のアニメーション速度 */
}
#digital-value-tree .btn-right:hover .tooltip{ /* マウスオーバー時のスタイル */
visibility: visible; /* ツールチップを表示 */
opacity: 1; /* 不透明度を100%に */
}
az network nic list --resource-group <リソースグループ名> --query "[].{NICName:name, PublicIPId:ipConfigurations[0].publicIpAddress.id}" -o tsv
az network public-ip show --ids <パブリックIP ID> --query "{PublicIP: ipAddress}"