Skip to content

Commit

Permalink
Merge pull request #41 from Halleck45/feat_charts
Browse files Browse the repository at this point in the history
feat: new HTML charts for loc, instability...
  • Loading branch information
Halleck45 committed Apr 3, 2024
2 parents 2ab5552 + a3d7199 commit 6abe945
Show file tree
Hide file tree
Showing 14 changed files with 490 additions and 8 deletions.
Binary file modified docs/preview-html-report.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 28 additions & 1 deletion src/Analyzer/Aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type Aggregated struct {
AverageMIPerMethod float64
AverageMIwocPerMethod float64
AverageMIcwPerMethod float64
AverageAfferentCoupling float64
AverageEfferentCoupling float64
AverageInstability float64
CommitCountForPeriod int
CommittedFilesCountForPeriod int // for example if one commit concerns 10 files, it will be 10
BusFactor int
Expand Down Expand Up @@ -124,6 +127,9 @@ func newAggregated() Aggregated {
AverageMIcw: 0,
AverageMIPerMethod: 0,
AverageMIwocPerMethod: 0,
AverageAfferentCoupling: 0,
AverageEfferentCoupling: 0,
AverageInstability: 0,
AverageMIcwPerMethod: 0,
CommitCountForPeriod: 0,
ResultOfGitAnalysis: nil,
Expand Down Expand Up @@ -211,6 +217,11 @@ func (r *Aggregator) consolidate(aggregated *Aggregated) {
aggregated.AverageMIcw = aggregated.AverageMIcw / float64(aggregated.NbClasses)
}

if aggregated.AverageInstability > 0 {
aggregated.AverageEfferentCoupling = aggregated.AverageEfferentCoupling / float64(aggregated.NbClasses)
aggregated.AverageAfferentCoupling = aggregated.AverageAfferentCoupling / float64(aggregated.NbClasses)
}

if aggregated.NbMethods > 0 {
aggregated.AverageLocPerMethod = aggregated.AverageLocPerMethod / float64(aggregated.NbMethods)
aggregated.AverageClocPerMethod = aggregated.AverageClocPerMethod / float64(aggregated.NbMethods)
Expand All @@ -231,6 +242,9 @@ func (r *Aggregator) consolidate(aggregated *Aggregated) {
aggregated.AverageMI = aggregated.AverageMIPerMethod
aggregated.AverageMIwoc = aggregated.AverageMIwocPerMethod
aggregated.AverageMIcw = aggregated.AverageMIcwPerMethod
aggregated.AverageInstability = 0
aggregated.AverageEfferentCoupling = 0
aggregated.AverageAfferentCoupling = 0
}

// Total locs: increment loc of each file
Expand Down Expand Up @@ -315,10 +329,17 @@ func (r *Aggregator) consolidate(aggregated *Aggregated) {
// Ce / (Ce + Ca)
instability := float32(class.Stmts.Analyze.Coupling.Efferent) / float32(class.Stmts.Analyze.Coupling.Efferent+class.Stmts.Analyze.Coupling.Afferent)
class.Stmts.Analyze.Coupling.Instability = instability

// to consolidate
aggregated.AverageInstability += float64(instability)
}
}
}

// Consolidate
aggregated.AverageInstability = aggregated.AverageInstability / float64(aggregated.NbClasses)


// Count commits for the period based on `ResultOfGitAnalysis` data
aggregated.ResultOfGitAnalysis = r.gitSummaries
if aggregated.ResultOfGitAnalysis != nil {
Expand Down Expand Up @@ -406,7 +427,6 @@ func (r *Aggregator) calculateSums(file *pb.File, specificAggregation *Aggregate
specificAggregation.AverageMIcwPerMethod += float64(*function.Stmts.Analyze.Maintainability.CommentWeight)
}
}

// average lines of code per method
if function.Stmts.Analyze.Volume != nil {
if function.Stmts.Analyze.Volume.Loc != nil {
Expand Down Expand Up @@ -441,6 +461,13 @@ func (r *Aggregator) calculateSums(file *pb.File, specificAggregation *Aggregate
}
}

// Coupling
if class.Stmts.Analyze.Coupling != nil {
specificAggregation.AverageInstability += float64(class.Stmts.Analyze.Coupling.Instability)
specificAggregation.AverageEfferentCoupling += float64(class.Stmts.Analyze.Coupling.Efferent)
specificAggregation.AverageAfferentCoupling += float64(class.Stmts.Analyze.Coupling.Afferent)
}

// cyclomatic complexity per class
if class.Stmts.Analyze.Complexity != nil && class.Stmts.Analyze.Complexity.Cyclomatic != nil {
specificAggregation.AverageCyclomaticComplexityPerClass += float64(*class.Stmts.Analyze.Complexity.Cyclomatic)
Expand Down
2 changes: 1 addition & 1 deletion src/Cli/ComponentBarchartCyclomaticByMethodRepartition.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (c *ComponentBarchartCyclomaticByMethodRepartition) GetData() *orderedmap.O
// render as HTML
func (c *ComponentBarchartCyclomaticByMethodRepartition) RenderHTML() string {
data := c.GetData()
return Engine.HtmlChartLine(data, "Cyclomatic complexity by method repartition", "chart-loc")
return Engine.HtmlChartLine(data, "Number of files", "chart-loc")
}

// Update is the method to update the component
Expand Down
14 changes: 13 additions & 1 deletion src/Report/Html/HtmlReportGenerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,19 @@ func (v *HtmlReportGenerator) Generate(files []*pb.File, projectAggregated Analy
return err
}

for _, file := range []string{"index.html", "layout.html", "risks.html", "componentTableRisks.html"} {
for _, file := range []string{
"index.html",
"layout.html",
"risks.html",
"componentChartRadiusBar.html",
"componentTableRisks.html",
"componentChartRadiusBarMaintainability.html",
"componentChartRadiusBarLoc.html",
"componentChartRadiusBarComplexity.html",
"componentChartRadiusBarInstability.html",
"componentChartRadiusBarEfferent.html",
"componentChartRadiusBarAfferent.html",
} {
// read the file
content, err := content.ReadFile(fmt.Sprintf("templates/%s", file))
if err != nil {
Expand Down
231 changes: 231 additions & 0 deletions src/Report/Html/templates/componentChartRadiusBar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
<div id="chart_bar_{{ chart_name }}" style="overflow:hidden"></div>
<script type="text/javascript">

// avoid blinking
var element = document.getElementById("chart_bar_{{ chart_name }}");
if (element){
var elementWidth = document.getElementById("chart_bar_{{ chart_name }}").clientWidth;
element.style.height = elementWidth + "px";
}

document.addEventListener('DOMContentLoaded', function() {
let json = document.getElementById("{{ chart_datasource_dom_element_identifier }}").textContent;
const domElement = document.getElementById("chart_bar_{{ chart_name }}");
json = json.replace(/},]/g, '}]'); // remove },]
let data = JSON.parse(json);

// find parent .chart-container element
const parent = domElement.closest(".chart-container");

const width = parent.clientWidth;
const padding = 0;
const height = width;
const innerRadius = 50;
const outerRadius = Math.min(width, height) * 0.40;

domElement.innerHTML = "";
domElement.style.width = (width + padding) + "px";
domElement.style.height = (height + padding) + "px";

// Create series from the data. Not stacked
const series = d3.stack()
.keys(["{{ chart_variable_name }}"])
.offset(d3.stackOffsetNone)
(data);

const arc = d3.arc()
.innerRadius(d => y(d[0]))
.outerRadius(d => y(d[1]))
.startAngle(d => x(d.data.name))
.endAngle(d => x(d.data.name) + x.bandwidth())
.padAngle(0.01)
.padRadius(innerRadius);

// Place bars all around a circle
const numBars = data.length;
const desiredBandwidth = numBars / (2 * Math.PI) ;
const x = d3.scaleBand()
.domain(data.map(d => d.name))
.range([0, desiredBandwidth])
.align(0);

// A radial y-scale maintains area proportionality of radial bars
const y = d3.scaleRadial()
.domain([0, d3.max(series, d => d3.max(d, d => d[1]))])
.range([innerRadius, outerRadius]);


const color = d3.scaleOrdinal()
.domain(series.map(d => d.key))
.range(d3.schemeCategory10);

// use only #1A56DB
color.range(["#1A56DB"]);

// A function to format the value in the tooltip
const formatValue = x => isNaN(x) ? "N/A" : x.toLocaleString("en")

const svg = d3.create("svg")
.attr("width", width )
.attr("height", height)
.attr("viewBox", [-(width + padding)/ 2, -height * 0.40, width, height])
.attr("style", "width: 100%; height: auto; font: 10px sans-serif;");

// A rect for each element in the series
svg.append("g")
.selectAll("g")
.data(series)
.join("g")
.attr("fill", d => color(d.key))
// animate on hover
.selectAll("path")
.data(d => d)
.join("path")
.attr("d", arc)
.append("title")
.text(function(d) {
return `${d.data.name}\n{{ chart_variable_label }}: ${formatValue(d.data.{{ chart_variable_name }})}`;
});

// animate on hover
svg.selectAll("path")
.on("mouseover", function(e, d) {

{% if chart_help_dom_element_identifier != nil && chart_help_dom_element_identifier != "" %}
// add help
const help = document.getElementById("{{ chart_help_dom_element_identifier }}");
help.innerHTML = `<b>${d.data.name}</b><br>{{ chart_variable_label }}: ${formatValue(d.data.{{ chart_variable_name }})}`;
{% endif %}

d3.select(this)
.transition()
.duration(100)
.attr("fill", "#0ea5e9");
})
.on("mouseout", function() {

{% if chart_help_dom_element_identifier != nil && chart_help_dom_element_identifier != "" %}
// clean help
const help = document.getElementById("{{ chart_help_dom_element_identifier }}");
help.innerHTML = "";
{% endif %}

d3.select(this)
.transition()
.duration(100)
.attr("fill", color);
});


// x axis
// only if we have less than 500 elements
if (data.length < 300) {
svg.append("g")
.attr("text-anchor", "middle")
// width and height
.attr("width", width)
.attr("height", height)
.selectAll()
.data(x.domain())
.join("g")
.attr("transform", d => `
rotate(${((x(d) + x.bandwidth() / 2) * 180 / Math.PI - 90)})
translate(${innerRadius},0)
`)
.call(g => g.append("line")
.attr("x2", -5)
.attr("stroke", "#000"))
}

// y axis
svg.append("g")
.attr("text-anchor", "end")
.call(g => g.selectAll("g")
.data(y.ticks(10).slice(1))
.join("g")
.attr("fill", "none")
.call(g => g.append("circle")
.attr("stroke", "#CCC")
.attr("stroke-opacity", 0.5)
.attr("r", y))
.call(g => g.append("text")
.attr("x", -6)
.attr("y", d => -y(d))
.attr("dy", "0.35em")
.attr("stroke", "#fff")
.attr("stroke-width", 5)
.text(y.tickFormat(10, "s"))
.clone(true)
.attr("fill", "#000")
.attr("stroke", "none")));

// color legend
svg.append("g")
.selectAll()
.data(color.domain())
.join("g")
.attr("transform", (d, i, nodes) => `translate(-40,${(nodes.length / 2 - i - 1) * 20})`)
.call(g => g.append("rect")
.attr("width", 18)
.attr("height", 18)
.attr("fill", color))
.call(g => g.append("text")
.attr("x", 24)
.attr("y", 9)
.attr("dy", "0.35em")
// .text(d => d));
.text("{{ chart_variable_label }}"));

// Click to zoom
let currentScale = 1;
const zoom = d3.zoom()
.scaleExtent([1, 8], [width, height])
.on("zoom", function(event) {
svg.attr("transform", event.transform);
});
svg.on("mousedown", function(event) {

// detect right click
if (event.button === 2) {
currentScale--;
if (currentScale < 1) {
currentScale = 1;
}
} else {
currentScale++;
}

// detect position of the mouse
const [x, y] = d3.pointer(event);
svg.transition()
.duration(750)
.call(zoom.scaleTo, currentScale, [x, y]);
});

// zoom out on scroll
svg.on("wheel", function(event) {

// only if currentScale is greater than 1 and if we are zooming out
if (currentScale === 1 && event.deltaY < 0) {
return;
}

event.preventDefault();
const [x, y] = d3.pointer(event);
currentScale--;
if (currentScale < 1) {
currentScale = 1;
}
svg.transition()
.duration(750)
.call(zoom.scaleTo, currentScale, [x, y]);
});

// add magnifier cursor
svg.style("cursor", "zoom-in");

const n = svg.node();
domElement.appendChild(n);
});

</script>
24 changes: 24 additions & 0 deletions src/Report/Html/templates/componentChartRadiusBarAfferent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script id="chart_afferent_data" type="application/json">
[
{%- set files = currentView.ConcernedFiles -%}
{%- for file in files -%}
{%- if len(file.Stmts.StmtClass) == 0 -%}
{% set elements = file|convertOneFileToCollection -%}
{% set name = file.Path %}
{%- else %}
{% set elements = file.Stmts.StmtClass -%}
{% set name = "" -%}
{%- endif -%}
{%- for class in elements -%}
{
"name": "{{ name|default:class.Name.Qualified|addslashes }}",
"afferent": {{ class.Stmts.Analyze.Coupling.Afferent|floatformat:0 }}
},
{%- endfor -%}
{%- endfor -%}
]
</script>

<div id="chart_bar_afferent_help" class="mb-2 italic text-sm text-gray-400 pt-4 bg-white text-center h-16 z-10 "></div>

{% include "componentChartRadiusBar.html" with chart_name="afferent" chart_variable_name="afferent" chart_variable_label="Afferent coupling" chart_datasource_dom_element_identifier="chart_afferent_data" chart_help_dom_element_identifier="chart_bar_afferent_help" %}
24 changes: 24 additions & 0 deletions src/Report/Html/templates/componentChartRadiusBarComplexity.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script id="chart_cyclo_data" type="application/json">
[
{%- set files = currentView.ConcernedFiles -%}
{%- for file in files -%}
{%- if len(file.Stmts.StmtClass) == 0 -%}
{% set elements = file|convertOneFileToCollection -%}
{% set name = file.Path %}
{%- else %}
{% set elements = file.Stmts.StmtClass -%}
{% set name = "" -%}
{%- endif -%}
{%- for class in elements -%}
{
"name": "{{ name|default:class.Name.Qualified|addslashes }}",
"cyclomatic": {{ class.Stmts.Analyze.Complexity.Cyclomatic|floatformat:0 }}
},
{%- endfor -%}
{%- endfor -%}
]
</script>

<div id="chart_bar_complexity_help" class="mb-2 italic text-sm text-gray-400 pt-4 bg-white text-center h-16 z-10 "></div>

{% include "componentChartRadiusBar.html" with chart_name="complexity" chart_variable_name="cyclomatic" chart_variable_label="Complexity" chart_datasource_dom_element_identifier="chart_cyclo_data" chart_help_dom_element_identifier="chart_bar_complexity_help" %}

0 comments on commit 6abe945

Please sign in to comment.