Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ebios stakeholders radar #1251

Merged
merged 11 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions backend/ebios_rm/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

LONG_CACHE_TTL = 60 # mn

import math


class BaseModelViewSet(AbstractBaseModelViewSet):
serializers_module = "ebios_rm.serializers"
Expand Down Expand Up @@ -157,6 +159,92 @@ class StakeholderViewSet(BaseModelViewSet):
def category(self, request):
return Response(dict(Stakeholder.Category.choices))

@action(detail=False, name="Get chart data")
def chart_data(self, request):
def get_exposure_segment_id(value):
if value < 3:
return 1
if value >= 3 and value < 7:
return 2
if value >= 7 and value <= 9:
return 3
if value > 9:
return 4
return 0

def get_reliability_cluster(value):
if value < 4:
return "clst1"
if value >= 4 and value < 6:
return "clst2"
if value >= 6 and value <= 7:
return "clst3"
if value > 7:
return "clst4"
return 1

"""
// data format: f1-f4 (fiabilité cyber = maturité x confiance ) to get the clusters and colors
// x,y, z
// x: criticité calculée avec cap à 5,5
// y: the angle (output of dict to make sure they end up on the right quadrant, min: 45, max:-45) -> done on BE
// z: the size of item (exposition = dependence x penetration) based on a dict, -> done on BE
// label: name of the 3rd party entity
Angles start at 56,25 (45+45/4) and end at -45-45/4 = 303,75
"""

# we can add a filter on the Stakeholder concerned by the ebios study here
qs = Stakeholder.objects.all()

c_data = {"clst1": [], "clst2": [], "clst3": [], "clst4": []}
r_data = {"clst1": [], "clst2": [], "clst3": [], "clst4": []}
angle_offsets = {"client": 135, "partner": 225, "supplier": 45}

cnt_c_not_displayed = 0
cnt_r_not_displayed = 0
for sh in qs:
# current
c_reliability = sh.current_maturity * sh.current_trust
c_exposure = sh.current_dependency * sh.current_penetration
c_exposure_val = get_exposure_segment_id(c_exposure) * 4

c_criticality = (
math.floor(sh.current_criticality * 100) / 100.0
if sh.current_criticality <= 5
else 5.25
)

angle = angle_offsets[sh.category] + (
get_exposure_segment_id(c_exposure) * (45 / 4)
)

vector = [c_criticality, angle, c_exposure_val, str(sh)]

cluser_id = get_reliability_cluster(c_reliability)
c_data[cluser_id].append(vector)

# residual
r_reliability = sh.residual_maturity * sh.residual_trust
r_exposure = sh.residual_dependency * sh.residual_penetration
r_exposure_val = get_exposure_segment_id(r_exposure) * 4

r_criticality = (
math.floor(sh.residual_criticality * 100) / 100.0
if sh.residual_criticality <= 5
else 5.25
)

angle = angle_offsets[sh.category] + (
get_exposure_segment_id(r_exposure) * (45 / 4)
)

vector = [r_criticality, angle, r_exposure_val, str(sh)]

cluser_id = get_reliability_cluster(r_reliability)
r_data[cluser_id].append(vector)

return Response({"current": c_data, "residual": r_data})


class StrategicScenarioViewSet(BaseModelViewSet):
model = StrategicScenario
Expand Down
289 changes: 289 additions & 0 deletions frontend/src/lib/components/Chart/EcosystemRadarChart.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
<script lang="ts">
import { onMount } from 'svelte';
import { safeTranslate } from '$lib/utils/i18n';
import { symbol } from 'zod';
import { grid } from '@unovis/ts/components/axis/style';

// export let name: string;

export let width = 'w-auto';
export let height = 'h-full';
export let classesContainer = 'border';
export let title = '';
export let name = '';
export let data;

// data format: f1-f4 (fiabilité cyber = maturité x confiance ) to get the clusters and colors
// x,y, z
// x: criticité calculée avec cap à 5,5
// y: the angle (output of dict to make sure they end up on the right quadrant, min: 45, max:-45) -> done on BE
// z: the size of item (exposition = dependence x penetration) based on a dict, -> done on BE
// label: name of the 3rd party entity
//

const chart_id = `${name}_div`;
onMount(async () => {
const echarts = await import('echarts');
let chart = echarts.init(document.getElementById(chart_id), null, { renderer: 'svg' });
const getGraphicElements = (chart) => {
const chartWidth = chart.getWidth();
const chartHeight = chart.getHeight();
const centerX = chartWidth / 2;
const centerY = chartHeight / 2;

return [
// Existing text elements
{
type: 'text',
position: [chartWidth / 4, (3 * chartHeight) / 4],
silent: true,
style: {
text: 'Prestataires',
font: '18px Arial',
fill: '#666',
textAlign: 'center',
textVerticalAlign: 'middle'
}
},
{
type: 'text',
position: [(3 * chartWidth) / 4, chartHeight / 4],
silent: true,
style: {
text: 'Partenaires',
font: '18px Arial',
fill: '#666',
textAlign: 'center',
textVerticalAlign: 'middle'
}
},
{
type: 'text',
position: [chartWidth / 4, chartHeight / 4],
silent: true,
style: {
text: 'Clients',
font: '18px Arial',
fill: '#666',
textAlign: 'center',
textVerticalAlign: 'middle'
}
}
];
};
const mainAngles = [45, 135, 225, 315];
const option = {
title: {
text: title
},
graphic: getGraphicElements(chart),
legend: {
data: ['<4', '4-5', '6-7', '>7'],
top: 60
},
polar: {},
tooltip: {
formatter: function (params) {
return params.value[3] + '<br/>Criticality: ' + params.value[0];
}
},
angleAxis: {
type: 'value',
startAngle: 315,
boundaryGap: true,
interval: 45,
axisLabel: { show: false },
splitLine: {
show: false
},
axisLine: {
show: false
},
axisTick: {
show: false
},
alignTick: true
},
radiusAxis: {
type: 'value',
max: 6,
inverse: true,
axisLabel: { show: true },
axisLine: {
show: true,
symbol: ['arrow', 'none'],
lineStyle: { width: 2 }
},
axisTick: {
show: false
}
},
series: [
{
name: '<4',
color: '#E73E51',
type: 'scatter',
coordinateSystem: 'polar',
symbolSize: function (val) {
return val[2] * 2;
},
data: data.clst1,
animationDelay: function (idx) {
return idx * 5;
}
},
{
name: '4-5',
color: '#DE8898',
type: 'scatter',
coordinateSystem: 'polar',
symbolSize: function (val) {
return val[2] * 2;
},
data: data.clst2,
animationDelay: function (idx) {
return idx * 5;
}
},
{
name: '6-7',
color: '#BAD9EA',
type: 'scatter',
coordinateSystem: 'polar',
symbolSize: function (val) {
return val[2] * 2;
},
data: data.clst3,
animationDelay: function (idx) {
return idx * 5;
}
},
{
name: '>7',
color: '#8A8B8A',
type: 'scatter',
coordinateSystem: 'polar',
symbolSize: function (val) {
return val[2] * 2;
},
data: data.clst4,
animationDelay: function (idx) {
return idx * 5;
}
},
{
name: 'Circle',
type: 'line',
coordinateSystem: 'polar',
itemStyle: { borderJoin: 'round' },
symbol: 'none',
data: new Array(360).fill(0).map((_, index) => {
return [2.5, index];
}),
lineStyle: {
color: '#E73E51',
width: 5
},
// If you don't want this to show up in the legend:
showInLegend: false,
silent: true,
zlevel: -1
},
{
name: 'Circle',
type: 'line',
coordinateSystem: 'polar',
symbol: 'none',
data: new Array(360).fill(0).map((_, index) => {
return [0.2, index];
}),
lineStyle: {
color: '#00ADA8',
width: 5
},
// If you don't want this to show up in the legend:
showInLegend: false,
silent: true,
zlevel: -1
},
{
name: 'Circle',
type: 'line',
coordinateSystem: 'polar',
symbol: 'none',
data: new Array(360).fill(0).map((_, index) => {
return [0.9, index];
}),
lineStyle: {
color: '#F8EA47',
width: 5
},
// If you don't want this to show up in the legend:
showInLegend: false,
silent: true,
zlevel: -1
},
// Center blue dot
{
name: 'CenterDot',
type: 'scatter',
coordinateSystem: 'polar',
symbol: 'circle',
symbolSize: 30,
itemStyle: {
color: '#007FB9',
borderWidth: 1
},
data: [[5.999, 0]],
silent: true,
zlevel: -1,
showInLegend: false
},
{
name: 'MinorSplitLines',
type: 'line',
coordinateSystem: 'polar',
symbol: 'none',
silent: true,
lineStyle: {
color: '#1C263B',
width: 1
},
data: mainAngles.flatMap((angle) => [
[0, angle],
[6, angle],
[NaN, NaN]
])
}
]
};

chart.setOption(option);

// Handle resize
window.addEventListener('resize', function () {
chart.resize();
// Update the graphic elements positions after resize
chart.setOption({
graphic: getGraphicElements(chart)
});
});

// Clean up event listener on component unmount
return () => {
window.removeEventListener('resize', function () {
chart.resize();
chart.setOption({
graphic: getGraphicElements(chart)
});
});
};
});
</script>

<div id={chart_id} class="{width} {height} {classesContainer}" />
{#if data.not_displayed > 0}
<div class="text-center">
⚠️ {data.not_displayed} items are not displayed as they are lacking data.
</div>
{/if}
Loading
Loading