diff --git a/package.json b/package.json
index f888075..f071218 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"@ngx-translate/core": "^12.1.2",
"@ngx-translate/http-loader": "^4.0.0",
"@swimlane/ngx-datatable": "^19.0.0",
+ "@types/d3": "^7.4.3",
"ajv": "^6.12.0",
"bootstrap": "^4.5.3",
"chart.js": "^2.9.3",
@@ -46,6 +47,7 @@
"chartjs-plugin-zoom": "^0.7.5",
"class-transformer": "^0.3.1",
"core-js": "^3.6.4",
+ "d3": "^7.9.0",
"dexie": "^3.0.3",
"dexie-export-import": "^1.0.0",
"file-saver": "^2.0.2",
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index eb7f1fd..3b30739 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -11,6 +11,7 @@ import { HomeNavLayoutComponent } from "@components/nav-layouts/home-nav-layout/
import { AuthGuard } from "@guards/auth.guard";
import { DataLoadingGuard } from "@guards/data-loading.guard";
import { DataProvidedGuard } from "@guards/data-provided.guard";
+import { PersonalComponent } from "./components/graphs/personal/personal.component";
const HOME_ROUTES: Routes = [
{ path: "home", component: HomeComponent },
@@ -34,6 +35,11 @@ const APP_ROUTES: Routes = [
canActivate: [AuthGuard, DataProvidedGuard],
component: QuestionsCompletionComponent,
},
+ {
+ path: "personal",
+ canActivate: [AuthGuard, DataProvidedGuard],
+ component: PersonalComponent,
+ },
{ path: "", redirectTo: "/home", pathMatch: "full" },
];
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 45a0686..425236f 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -49,6 +49,7 @@ import { HelpNavItemComponent } from "./components/nav-items/help-nav-item/help-
import { AppNavLayoutComponent } from "./components/nav-layouts/app-nav-layout/app-nav-layout.component";
import { HomeNavLayoutComponent } from "./components/nav-layouts/home-nav-layout/home-nav-layout.component";
import { OverviewGraphContextualMenuComponent } from "./components/overview-graph-contextual-menu/overview-graph-contextual-menu.component";
+import { PersonalComponent } from "./components/graphs/personal/personal.component";
/**
* Firebase configuration file
@@ -112,6 +113,7 @@ export function appInitializerFactory(
OverviewComponent,
StudentsCommitsComponent,
QuestionsCompletionComponent,
+ PersonalComponent,
MetadataComponent,
EditRepositoriesComponent,
ConfigurationComponent,
diff --git a/src/app/components/graphs/overview/chart.scss b/src/app/components/graphs/overview/chart.scss
new file mode 100644
index 0000000..ecf93dd
--- /dev/null
+++ b/src/app/components/graphs/overview/chart.scss
@@ -0,0 +1,104 @@
+.hidden {
+ display: none;
+}
+
+text.repo_name {
+ font-size: 18px;
+ pointer-events: all;
+ cursor: pointer;
+}
+
+text.repo_name:hover {
+ font-size: 16px;
+}
+
+.repository {
+ clip-path: url(#clip);
+}
+
+.tick_line {
+ stroke-width: 1px;
+ stroke: #e5e5e5;
+}
+
+.tick > line {
+ stroke-width: 1px;
+ stroke: #e5e5e5;
+}
+
+.axis > line {
+ stroke-width: 1px;
+ stroke: #e5e5e5;
+}
+
+.commit {
+ stroke: #fff;
+ stroke-width: 2px;
+}
+
+.commit-cloture {
+ r: 7px;
+}
+
+.commit-normal {
+ width: 12px;
+ height: 12px;
+ transform-box: fill-box;
+ transform-origin: center;
+ transform: translate(-6px, -6px) rotate(45deg);
+}
+
+.commit-normal:hover {
+ fill: white;
+ stroke: black;
+}
+
+.commit_line {
+ stroke: #999;
+ stroke-width: 2px;
+}
+
+.session {
+ stroke-width: 2px;
+ stroke: rgba(79, 195, 247, 1);
+ fill: rgba(33, 150, 243, 0.15);
+}
+
+.review {
+ stroke: blue;
+ fill: blue;
+ background-color: blue;
+}
+
+.correction {
+ stroke: red;
+ fill: red;
+ background-color: red;
+}
+
+.other {
+ stroke: black;
+ fill: black;
+ background-color: black;
+}
+
+.milestone {
+ pointer-events: auto;
+}
+
+.milestone > .milestone_label {
+ border-radius: 10;
+}
+
+.milestone > rect {
+ rx: 5px;
+ ry: 5px;
+}
+
+.milestone > text {
+ padding: 5px;
+ color: white;
+ fill: white;
+ stroke: white;
+ user-select: none;
+}
\ No newline at end of file
diff --git a/src/app/components/graphs/overview/overview.component.html b/src/app/components/graphs/overview/overview.component.html
index d832473..3c9d301 100644
--- a/src/app/components/graphs/overview/overview.component.html
+++ b/src/app/components/graphs/overview/overview.component.html
@@ -1,153 +1,315 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
-
-
-
-
-
- {{ 'OVERVIEW-GRAPH.LEGEND.COMMIT' | translate }}
- {{ 'OVERVIEW-GRAPH.LEGEND.CLOSING' | translate }}
-
- {{ 'OVERVIEW-GRAPH.LEGEND.INTERMEDIATE' | translate }}
-
- {{ 'OVERVIEW-GRAPH.LEGEND.BEFORE' | translate }}
-
- {{ 'OVERVIEW-GRAPH.LEGEND.BETWEEN' | translate }}
-
- {{ 'OVERVIEW-GRAPH.LEGEND.AFTER' | translate }}
-
- {{ 'REVIEW' | translate }}
-
- {{ 'CORRECTION' | translate }}
-
- {{ 'OTHER' | translate }}
-
-
-
- {{ 'SESSION' | translate }}
-
-
+
+
+
+
+
+ {{ "OVERVIEW-GRAPH.LEGEND.COMMIT" | translate }}
+
+ {{ "OVERVIEW-GRAPH.LEGEND.CLOSING" | translate }}
+
+ {{ "OVERVIEW-GRAPH.LEGEND.INTERMEDIATE" | translate }}
+
+ {{ "OVERVIEW-GRAPH.LEGEND.BEFORE" | translate }}
+
+ {{ "OVERVIEW-GRAPH.LEGEND.BETWEEN" | translate }}
+
+ {{ "OVERVIEW-GRAPH.LEGEND.AFTER" | translate }}
+
+ {{ "REVIEW" | translate }}
+
+ {{ "CORRECTION" | translate }}
+
+ {{ "OTHER" | translate }}
+
+
+
+
+ {{ "SESSION" | translate }}
+
+
-
+
-{{ 'OVERVIEW-GRAPH.MARKERS.TOOLTIP' | translate }}
-{{ 'OVERVIEW-GRAPH.RESET-ZOOM-TOOLTIP' | translate }}
-{{ 'OVERVIEW-GRAPH.CHANGE-ZOOM-TOOLTIP' | translate }}
-{{ 'OVERVIEW-GRAPH.CHANGE-TIME-SCALE-TOOLTIP' | translate }}
-{{ 'OVERVIEW-GRAPH.RELOAD-TOOLTIP' | translate }}
+{{
+ "OVERVIEW-GRAPH.MARKERS.TOOLTIP" | translate
+}}
+{{
+ "OVERVIEW-GRAPH.RESET-ZOOM-TOOLTIP" | translate
+}}
+{{
+ "OVERVIEW-GRAPH.CHANGE-ZOOM-TOOLTIP" | translate
+}}
+{{
+ "OVERVIEW-GRAPH.CHANGE-TIME-SCALE-TOOLTIP" | translate
+}}
+{{
+ "OVERVIEW-GRAPH.RELOAD-TOOLTIP" | translate
+}}
diff --git a/src/app/components/graphs/overview/overview.component.scss b/src/app/components/graphs/overview/overview.component.scss
index e69de29..bcdb1cf 100644
--- a/src/app/components/graphs/overview/overview.component.scss
+++ b/src/app/components/graphs/overview/overview.component.scss
@@ -0,0 +1,90 @@
+#chart {
+ --top-inner: 30px;
+ --bottom-inner: 20px;
+
+ --chart-height: 100%;
+
+ --repo-space: 30px;
+
+ min-height: min(calc(var(--top-inner) + var(--bottom-inner)), 100px);
+ max-height: var(--chart-height);
+
+ width: 100%;
+ --chart-width-left-spacing-ratio: 90%;
+ --chart-width-max-left-spacing: 50px;
+ // to change chart-with
+
+ height: 620px;
+}
+
+.chart-container {
+ position: relative;
+ top: var(--top-inner);
+ width: 100%;
+ height: 100%;
+ min-height: 0px;
+ max-height: calc(600px - var(--top-inner) - var(--bottom-inner));
+ vertical-align: top;
+ overflow: auto;
+ overscroll-behavior: none;
+}
+
+.chart-container-absolute {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ min-height: 0px;
+ max-height: 660px;
+ vertical-align: top;
+ pointer-events: none;
+ overflow: hidden;
+}
+
+.chart-container-absolute>svg {
+ position: relative;
+}
+
+.chart-container:focus {
+ outline: none;
+}
+
+#tooltip {
+ position: absolute;
+}
+
+#commit_hover {
+ width: fit-content;
+ color: white;
+ background-color: rgba(0, 0, 0, 0.7);
+ font-size: 0;
+ padding: 5px;
+ border-radius: 5px;
+ transform: translateY(-90px);
+ user-select: none;
+}
+
+#commit_hover>p {
+ font-size: 10px;
+ margin: 2px;
+}
+
+#commit_group_hover {
+ transform: translateY(-90px);
+ width: fit-content;
+}
+
+#commit_group_hover>div {
+ color: white;
+ background-color: rgba(0, 0, 0, 0.7);
+ font-size: 0;
+ padding: 5px;
+ border-radius: 5px;
+}
+
+
+#commit_group_hover>div>p {
+ font-size: 10px;
+ margin: 2px;
+}
\ No newline at end of file
diff --git a/src/app/components/graphs/overview/overview.component.ts b/src/app/components/graphs/overview/overview.component.ts
index 4cc22b5..d4d5097 100644
--- a/src/app/components/graphs/overview/overview.component.ts
+++ b/src/app/components/graphs/overview/overview.component.ts
@@ -5,6 +5,7 @@ import {
OnDestroy,
OnInit,
ViewChild,
+ ViewEncapsulation,
} from "@angular/core";
import { FileChooserComponent } from "@components/file-chooser/file-chooser.component";
import { OverviewGraphContextualMenuComponent } from "@components/overview-graph-contextual-menu/overview-graph-contextual-menu.component";
@@ -18,24 +19,29 @@ import { DataService } from "@services/data.service";
import { JsonManagerService } from "@services/json-manager.service";
import { LoaderService } from "@services/loader.service";
import { ToastService } from "@services/toast.service";
-import { BaseChartDirective } from "ng2-charts";
-import { Subscription } from "rxjs";
+import { Subscription, concat } from "rxjs";
import { BaseGraphComponent } from "../base-graph.component";
+import * as d3 from "d3";
+import { Repository } from "../../../models/Repository.model";
+import { tick } from "@angular/core/testing";
+import { rejects } from "assert";
+import { Utils } from "../../../services/utils";
+import { EmotionService } from "../../../services/emotion.service";
+import { ActivatedRoute, Router } from "@angular/router";
+
@Component({
selector: "overview",
templateUrl: "./overview.component.html",
- styleUrls: ["./overview.component.scss"],
+ styleUrls: ["./overview.component.scss", "./chart.scss"],
+ encapsulation: ViewEncapsulation.None,
})
export class OverviewComponent
extends BaseGraphComponent
- implements OnInit, AfterViewInit, OnDestroy
-{
- @ViewChild(BaseChartDirective, { static: true }) myChart;
+ implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(OverviewGraphContextualMenuComponent) contextualMenu;
minZoom: number;
- maxZoom: number;
contextualMenuShown: boolean;
@@ -60,116 +66,66 @@ export class OverviewComponent
typeModal: string;
addModal: boolean;
savedMilestoneModal: Milestone;
- ////////////////////////
- chartOptions = {
- responsive: true,
- aspectRatio: 2.4,
- animation: {
- duration: 0,
- },
- responsiveAnimationDuration: 0,
- hover: {
- mode: "nearest",
- intersec: true,
- animationDuration: 0,
- },
- interaction: {
- mode: "nearest",
- },
- tooltips: {
- callbacks: {
- label(tooltipItem, data) {
- return "";
- },
- beforeBody(tooltipItem, data) {
- const commit =
- data.datasets[tooltipItem[0].datasetIndex].data[
- tooltipItem[0].index
- ].commit;
- return commit.message + "\n\n" + commit.author;
- },
- },
- displayColors: false,
- },
- elements: {
- line: {
- fill: false,
- borderWidth: 2,
- tension: 0,
- },
- point: {
- hitRadius: 8,
- radius: 6,
- },
- },
- scales: {
- xAxes: [
- {
- offset: true,
- type: "time",
- time: {
- unit: this.unit,
- tooltipFormat: "DD/MM/YY HH:mm",
- displayFormats: {
- day: "DD/MM/YY",
- week: "DD/MM/YY",
- hour: "kk:mm",
- },
- },
- },
- ],
- yAxes: [
- {
- type: "category",
- labels: [],
- offset: true,
- },
- ],
- },
- annotation: {
- drawTime: "beforeDatasetsDraw",
- events: ["click", "enter", "leave"],
- annotations: [],
- },
- plugins: {
- zoom: {
- pan: {
- enabled: false,
- mode: "x",
- onPan: ({ chart }) => {
- this.saveMinMaxZoom(
- chart.scales["x-axis-0"].min,
- chart.scales["x-axis-0"].max
- );
- },
- },
- zoom: {
- enabled: true,
- drag: {
- borderColor: "rgba(225,225,225,0.3)",
- borderWidth: 5,
- backgroundColor: "rgb(225,225,225)",
- },
- mode: "x",
- speed: 0.3,
- onZoom: ({ chart }) => {
- this.saveMinMaxZoom(
- chart.scales["x-axis-0"].min,
- chart.scales["x-axis-0"].max
- );
- this.adaptScaleWithChart(chart);
- },
- },
- },
- },
- };
+ // params
+ inner_margin;
+ margin_abs;
+ width;
+ height;
+ maxZoom: number;
+
+ // svg components
+ svg: d3.Selection;
+ scrollable: d3.Selection;
+ chart_svg: d3.Selection;
+ x_g: d3.Selection;
+ y_g: d3.Selection;
+ repository_g: d3.Selection;
+ repositories_g: d3.Selection[];
+ axis_g: d3.Selection;
+ axis_abs_g: d3.Selection;
+ other_g: d3.Selection;
+ session_g: d3.Selection;
+ review_g: d3.Selection;
+ correction_g: d3.Selection;
+ commits_line_g: d3.Selection;
+ data_g: d3.Selection;
+ commits_g: d3.Selection;
+
+ x_scale: d3.ScaleTime;
+ x_scale_copy: d3.ScaleTime; // Used by zooming
+ x_axis: d3.Axis;
+ y_scale: d3.ScaleLinear;
+ y_axis: d3.Axis;
+
+ clip: d3.Selection;
+ zoom: d3.ZoomBehavior;
+
+ hovered_commit: Commit;
+ hovered_group_commit: Commit[];
+ hovered_g: d3.Selection;
+
+ static GROUP_HEIGHT = 12;
+ static CIRCLE_RADIUS = 12;
+ brush: d3.BrushBehavior;
+ current_zoom: any;
+ chart_abs_g: d3.Selection;
+ svg_abs: d3.Selection;
+ real_height: number;
+ chart_width: number;
+ repo_spacing: number;
+ inner_width: number;
+ inner_height: number;
+ scrollable_height: number;
+ ////////////////////////
constructor(
private translateService: TranslateService,
private toastService: ToastService,
public jsonManager: JsonManagerService,
public dataService: DataService,
+ public router: Router,
+ public emotionService: EmotionService,
protected loaderService: LoaderService,
private modalService: NgbModal,
protected assignmentsService: AssignmentsService
@@ -177,13 +133,6 @@ export class OverviewComponent
super(loaderService, assignmentsService, dataService);
}
- @HostListener("window:keyup", ["$event"])
- keyEvent(event: KeyboardEvent) {
- if (event.keyCode === 32) {
- this.resetZoom();
- }
- }
-
ngOnInit(): void {
this.defaultSessionDuration =
this.dataService.assignment.defaultSessionDuration;
@@ -199,9 +148,51 @@ export class OverviewComponent
ngOnDestroy(): void {
this.unsubscribeAssignmentModified(this.assignmentsModified$);
+ d3.select(window).on("resize", null);
+ }
+
+ getDisplayedRepositories(): Repository[] {
+ return this.dataService.repositories.filter(
+ (repository) =>
+ !this.dataService.groupFilter ||
+ repository.tpGroup === this.dataService.groupFilter
+ );
+ }
+
+ commit_date_format = Utils.COMMIT_DATE_FORMAT;
+
+ updateVariableFromCss(): void {
+ let chart_div = document.getElementById("chart");
+
+ var style = getComputedStyle(chart_div);
+
+ var css_var_number = (name: string, dash = true) =>
+ Number.parseInt(style.getPropertyValue((dash ? "--" : "") + name));
+
+ let rect = chart_div.getBoundingClientRect();
+ this.width = rect.width;
+ this.height = rect.height;
+
+ this.chart_width = Math.min(
+ (css_var_number("chart-width-left-spacing-ratio") * this.width) / 100,
+ this.width - css_var_number("chart-width-max-left-spacing")
+ );
+
+ this.inner_margin = {
+ top: css_var_number("top-inner"),
+ bottom: css_var_number("bottom-inner"),
+ };
+
+ this.inner_width = this.width;
+ this.inner_height =
+ this.height - this.inner_margin.top - this.inner_margin.bottom;
+
+ this.repo_spacing = css_var_number("repo-space");
}
ngAfterViewInit(): void {
+ this.refresh();
+
setTimeout(() => {
if (this.dataService.repoToLoad) {
this.loadGraph(this.dataService.startDate, this.dataService.endDate);
@@ -216,6 +207,8 @@ export class OverviewComponent
this.loading = false;
}
+
+ this.refreshElementState();
});
}
@@ -245,36 +238,179 @@ export class OverviewComponent
}
}
+ refreshTooltip(x?: number, y?: number) {
+ if (x == null || y == null) {
+ return;
+ }
+ var tooltip = document.getElementById("tooltip");
+ if (tooltip == null) {
+ return;
+ }
+
+ tooltip.style.top = y + 20 + "px";
+ tooltip.style.left = x + 20 + "px";
+
+ if (this.hovered_g) {
+ if (this.hovered_g.select(":hover").empty()) {
+ this.hovered_commit = undefined;
+ this.hovered_group_commit = undefined;
+ this.hovered_g = null;
+ }
+ }
+ }
+
loadGraphData() {
this.loadAnnotations();
this.loadPoints();
- setTimeout(() => {
- this.adaptScaleWithChart(this.myChart.chart);
- });
+ this.setupZoom();
+ }
+
+ setupZoom() {
+ const overview = this;
+ this.zoom = d3
+ .zoom()
+ .on("zoom", (event) => {
+ if (overview.drag) {
+ return;
+ }
+
+ if (event.sourceEvent != null) {
+ overview.refreshTooltip(
+ event.sourceEvent.clientX,
+ event.sourceEvent.clientY
+ );
+ }
+
+ overview.current_zoom = event.transform;
+ overview.x_scale_copy = overview.current_zoom.rescaleX(
+ overview.x_scale
+ );
+ overview.x_g.call(this.x_axis.scale(overview.x_scale_copy));
+ overview.refreshElementState();
+ })
+ .filter((event) => {
+ return event.shiftKey || !(event instanceof WheelEvent);
+ })
+ .scaleExtent([0.5, overview.maxZoom]);
+
+ this.data_g = this.data_g.call(this.zoom).on("dblclick.zoom", null);
+
+ this.resetZoom(true);
+ }
+
+ refresh() {
+ this.updateVariableFromCss();
+ this.scrollable_height = Math.max(
+ this.height - this.inner_margin.top - this.inner_margin.bottom,
+ this.getDisplayedRepositories().length * this.repo_spacing
+ );
+
+ d3.select(window).on("resize", () => this.loadGraphDataAndRefresh());
+
+ this.svg = d3
+ .select(".chart-container")
+ .append("svg")
+ .attr("preserveAspectRatio", "none")
+ .attr("viewBox", `0 0 ${this.width} ${this.scrollable_height}`);
+
+ this.svg_abs = d3
+ .select(".chart-container-absolute")
+ .append("svg")
+ .attr("preserveAspectRatio", "none")
+ .attr("viewBox", `0 0 ${this.width} ${this.height}`);
+
+ const overview = this;
+
+ const translation = [this.width - this.chart_width, 0];
+
+ this.chart_svg = this.svg
+ .append("g")
+ .attr("transform", "translate(" + translation + ")");
+
+ this.data_g = this.chart_svg.append("g");
+
+ this.chart_abs_g = this.svg_abs
+ .append("g")
+ .attr("transform", "translate(" + translation + ")");
+
+ this.data_g
+ .append("rect")
+ .attr("id", "data")
+ .attr("width", this.inner_width)
+ .attr("height", this.scrollable_height)
+ .attr("opacity", "0")
+ .on("click", (event: MouseEvent) => {
+ event.stopPropagation();
+ var rect = (event.target as any).getBoundingClientRect();
+ var x =
+ ((event.clientX - rect.left) / (rect.right - rect.left)) *
+ overview.inner_width; //x position within the element.
+ let rawDate = overview.x_scale_copy.invert(x);
+ this.openContextMenu(event.pageX, event.pageY, rawDate);
+ });
+
+ d3.select(".chart-container")
+ .on("mousemove", function (e) {
+ overview.refreshTooltip(e.clientX, e.clientY);
+ })
+ .on("scroll", () => this.refreshElementState())
+ .attr("tabindex", "0")
+ .attr("focusable", "true")
+ .on("keypress", (event) => {
+ if (event.keyCode === 32) {
+ this.resetZoom(false);
+ }
+ });
+
+ this.clip = this.chart_svg
+ .append("defs")
+ .append("svg:clipPath")
+ .attr("id", "clip")
+ .append("svg:rect")
+ .attr("width", this.width)
+ .attr("height", 2 * this.scrollable_height)
+ .attr("fill", "black")
+ .attr("x", 0)
+ .attr("y", -this.scrollable_height);
}
loadGraphDataAndRefresh() {
+ this.svg.remove();
+ this.svg_abs.remove();
+
+ this.refresh();
+
this.loadGraphData();
- this.refreshGraph();
- this.adaptScaleWithChart(this.myChart.chart);
}
loadAnnotations() {
- this.chartOptions.annotation.annotations = [];
+ let milestone_filter = (review: Milestone) =>
+ (!this.dataService.groupFilter ||
+ !review.tpGroup ||
+ review.tpGroup === this.dataService.groupFilter) &&
+ (!this.searchFilter.length ||
+ this.searchFilter.filter((question) =>
+ review.questions?.includes(question)
+ ).length);
+
+ if (this.session_g != null) this.session_g.remove();
+ if (this.review_g != null) this.session_g.remove();
+ if (this.correction_g != null) this.session_g.remove();
+ if (this.other_g != null) this.session_g.remove();
if (this.dataService.sessions && this.showSessions) {
this.loadSessions();
}
if (this.dataService.reviews && this.showReviews) {
- this.loadReviews();
+ this.loadReviews(milestone_filter);
}
if (this.dataService.corrections && this.showCorrections) {
- this.loadCorrections();
+ this.loadCorrections(milestone_filter);
}
if (this.dataService.others && this.showOthers) {
- this.loadOthers();
+ this.loadOthers(milestone_filter);
}
}
@@ -304,7 +440,11 @@ export class OverviewComponent
openContextMenu(x: number, y: number, date: Date) {
if (!this.isContextualMenuShown()) {
- this.contextualMenu.openNew(x, y, date);
+ try {
+ this.contextualMenu.openNew(x, y, date);
+ } catch (error) { }
+ } else {
+ this.contextualMenu.close();
}
}
@@ -429,307 +569,800 @@ export class OverviewComponent
);
}
- loadSessions() {
- let me = this;
- this.dataService.sessions
- .filter(
- (session) =>
- !this.dataService.groupFilter ||
- !session.tpGroup ||
- session.tpGroup === this.dataService.groupFilter
+ getRectForSession(g: d3.Selection, session: Session) {
+ const overview = this;
+ g.append("rect")
+ .datum(session)
+ .attr("class", "session")
+ .attr("clip-path", "url(#clip)")
+ .attr("x", this.xScaledTimeZoned(session.startDate))
+ .attr("height", 100)
+ .attr("y", this.inner_margin.bottom + this.inner_height)
+ .attr(
+ "width",
+ this.xScaledTimeZoned(session.endDate) -
+ this.xScaledTimeZoned(session.startDate)
)
- .forEach((session) => {
- this.chartOptions.annotation.annotations.push({
- type: "box",
- xScaleID: "x-axis-0",
- yScaleID: "y-axis-0",
- xMin: session.startDate,
- xMax: session.endDate,
- borderColor: "rgba(79, 195, 247,1.0)",
- borderWidth: 2,
- backgroundColor: "rgba(33, 150, 243, 0.15)",
- onClick: function (e) {
- const rawDate = me.getDateFromEvent(e);
- me.openEditSessionContextMenu(session, e.pageX, e.pageY, rawDate);
- },
+ .on("click", (e) =>
+ overview.openEditSessionContextMenu(
+ session,
+ e.pageX,
+ e.pageY,
+ overview.x_scale.invert(e.pageX)
+ )
+ );
+ }
+
+ loadSessions() {
+ let loaded_sessions: Session[] = this.dataService.sessions.filter(
+ (session) =>
+ !this.dataService.groupFilter ||
+ !session.tpGroup ||
+ session.tpGroup === this.dataService.groupFilter
+ );
+
+ this.session_g = this.chart_abs_g.append("g");
+
+ const overview = this;
+
+ setTimeout(() => {
+ this.session_g
+ .selectAll(".session")
+ .data(loaded_sessions)
+ .enter()
+ .each(function (d: Session) {
+ overview.getRectForSession(d3.select(this), d);
});
+ });
+ }
+
+ getLineForMilestone(
+ parent: d3.Selection,
+ m: Milestone,
+ class_: string,
+ index: number
+ ) {
+ const overview = this;
+ let g = parent.append("g").attr("class", class_);
+
+ // Line
+ g.append("rect")
+ // .attr("clip-path", "url(#clip)")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", 1)
+ .attr("height", this.inner_height)
+ .attr("transform", "translate(" + [-1, 0] + ")");
+
+ // Box
+ let box = g.append("rect").attr("y", 0);
+
+ // Text
+ let text = g
+ .append("text")
+ .attr("y", -8)
+ .text(m.label || m.type.substring(0, m.type.length - 1) + " " + index)
+ .attr("text-anchor", "middle");
+
+ let bbox = text.node().getBBox();
+
+ bbox.width += 4;
+ bbox.height += 5;
+ bbox.x -= 2;
+ bbox.y -= 1;
+
+ box.attr("width", bbox.width);
+ box.attr("height", bbox.height);
+ box.attr("x", -bbox.width / 2);
+ box.attr("y", bbox.y);
+
+ let x = this.xScaledTimeZoned(m.date);
+
+ return g
+ .attr("transform", `translate(${x}, ${this.inner_margin.top})`)
+ .call((g) => g.classed("hidden", x < 0 || x > overview.width))
+ .on("click", (e, d: Milestone) => {
+ e.stopPropagation();
+ const rawDate = this.x_scale.invert(e.pageX);
+ overview.openEditMilestoneContextMenu(d, e.pageX, e.pageY, rawDate); //, rawDate)
});
}
- loadReviews() {
- let me = this;
- this.dataService.reviews
- .filter(
- (review) =>
- (!this.dataService.groupFilter ||
- !review.tpGroup ||
- review.tpGroup === this.dataService.groupFilter) &&
- (!this.searchFilter.length ||
- this.searchFilter.filter((question) =>
- review.questions?.includes(question)
- ).length)
- )
- .forEach((review, index) => {
- this.chartOptions.annotation.annotations.push({
- type: "line",
- mode: "vertical",
- scaleID: "x-axis-0",
- value: review.date,
- borderColor: "blue",
- borderWidth: 1,
- label: {
- content: review.label || "Review " + (index + 1),
- enabled: true,
- position: "top",
- },
- onClick: function (e) {
- const rawDate = me.getDateFromEvent(e);
- me.openEditMilestoneContextMenu(review, e.pageX, e.pageY, rawDate);
- },
+ loadReviews(milestone_filter: (review: Milestone) => number | boolean) {
+ let loaded_reviews = this.dataService.reviews.filter(milestone_filter);
+
+ this.review_g = this.chart_abs_g.append("g");
+
+ const overview = this;
+
+ setTimeout(() => {
+ this.review_g
+ .selectAll(".review")
+ .data(loaded_reviews)
+ .enter()
+ .each(function (d: Milestone, i) {
+ overview.getLineForMilestone(
+ d3.select(this),
+ d,
+ "milestone review",
+ i
+ );
});
- });
+ });
}
- loadCorrections() {
- let me = this;
- this.dataService.corrections
- .filter(
- (correction) =>
- (!this.dataService.groupFilter ||
- !correction.tpGroup ||
- correction.tpGroup === this.dataService.groupFilter) &&
- (!this.searchFilter.length ||
- this.searchFilter.filter((question) =>
- correction.questions?.includes(question)
- ).length)
- )
- .forEach((correction, index) => {
- this.chartOptions.annotation.annotations.push({
- type: "line",
- mode: "vertical",
- scaleID: "x-axis-0",
- value: correction.date,
- borderColor: "red",
- borderWidth: 1,
- label: {
- content: correction.label || "Correction " + (index + 1),
- enabled: true,
- position: "top",
- },
- onClick: function (e) {
- const rawDate = me.getDateFromEvent(e);
- me.openEditMilestoneContextMenu(
- correction,
- e.pageX,
- e.pageY,
- rawDate
- );
- },
+ loadCorrections(milestone_filter: (review: Milestone) => number | boolean) {
+ let loaded_corrections =
+ this.dataService.corrections.filter(milestone_filter);
+
+ this.correction_g = this.chart_abs_g.append("g");
+
+ const overview = this;
+
+ setTimeout(() => {
+ this.correction_g
+ .selectAll(".correction")
+ .data(loaded_corrections)
+ .enter()
+ .each(function (d: Milestone, i) {
+ overview.getLineForMilestone(
+ d3.select(this),
+ d,
+ "milestone correction",
+ i
+ );
});
- });
+ });
}
- loadOthers() {
- let me = this;
- this.dataService.others
- .filter(
- (other) =>
- (!this.dataService.groupFilter ||
- !other.tpGroup ||
- other.tpGroup === this.dataService.groupFilter) &&
- (!this.searchFilter.length ||
- this.searchFilter.filter((question) =>
- other.questions?.includes(question)
- ).length)
- )
- .forEach((other, index) => {
- this.chartOptions.annotation.annotations.push({
- type: "line",
- mode: "vertical",
- scaleID: "x-axis-0",
- value: other.date,
- borderColor: "black",
- borderWidth: 1,
- label: {
- content: other.label || "Other " + (index + 1),
- enabled: true,
- position: "top",
- },
- onClick: function (e) {
- const rawDate = me.getDateFromEvent(e);
- me.openEditMilestoneContextMenu(other, e.pageX, e.pageY, rawDate);
- },
+ loadOthers(milestone_filter: (review: Milestone) => number | boolean) {
+ let loaded_other = this.dataService.others.filter(milestone_filter);
+
+ this.other_g = this.chart_abs_g.append("g");
+
+ const overview = this;
+
+ setTimeout(() => {
+ this.other_g
+ .selectAll(".other")
+ .data(loaded_other)
+ .enter()
+ .each(function (d: Milestone, i) {
+ overview.getLineForMilestone(
+ d3.select(this),
+ d,
+ "milestone other",
+ i
+ );
});
- });
+ });
}
- loadPoints() {
- const chartData = [];
- const labels = [];
-
- this.dataService.repositories
- .filter(
- (repository) =>
- !this.dataService.groupFilter ||
- repository.tpGroup === this.dataService.groupFilter
+ setupAxis(repositories: Repository[], minDate: Date, maxDate: Date) {
+ if (this.axis_g != null) this.axis_g.remove();
+ const overview = this;
+
+ this.x_scale = d3
+ .scaleTime()
+ .domain([minDate, maxDate])
+ .range([0, this.inner_width])
+ .nice();
+
+ this.x_scale_copy = this.x_scale.copy();
+
+ this.x_axis = d3
+ .axisBottom(this.x_scale_copy)
+ .ticks(6)
+ .tickSize(-this.inner_height);
+
+ this.x_axis.tickFormat(function (d) {
+ if (!(d instanceof Date)) return "";
+ let ticks = overview.x_scale_copy.ticks();
+ if (ticks[ticks.length - 1] == null || ticks[0] == null) return "";
+ let spacing =
+ (ticks[ticks.length - 1].getTime() - ticks[0].getTime()) / 1000;
+
+ return OverviewComponent.multiFormat(spacing, d);
+ });
+
+ this.axis_g = this.chart_svg.insert("g", ":first-child");
+ this.axis_abs_g = this.chart_abs_g.insert("g", ":first-child");
+
+ this.x_g = this.axis_abs_g
+ .append("g")
+ .attr(
+ "transform",
+ "translate(" + [0, this.inner_height + this.inner_margin.bottom] + ")"
)
- .forEach((repository) => {
- const data = [];
- const pointStyle = [];
- const pointBackgroundColor = [];
- const borderColor = "rgba(77, 77, 77, 0.5)";
- labels.push(repository.getDisplayName());
- repository.commits &&
- repository.commits.forEach((commit) => {
- if (
- !this.searchFilter.length ||
- this.searchFilter.includes(commit.question)
- ) {
- data.push({
- x: commit.commitDate,
- y: repository.getDisplayName(),
- commit,
- });
- pointStyle.push(this.getPointStyle(commit));
- pointBackgroundColor.push(commit.color.color);
- }
- });
- chartData.push({
- data,
- pointStyle,
- pointBackgroundColor,
- borderColor,
- });
+ .call(this.x_axis);
+
+ this.y_scale = d3
+ .scaleLinear()
+ .domain([0, repositories.length + 1])
+ .range([0, this.scrollable_height]);
+
+ this.y_axis = d3
+ .axisLeft(this.y_scale)
+ .tickValues([...Array(repositories.length + 1).keys()])
+ .tickFormat((d) => repositories[d.valueOf() - 1]?.name || "")
+ .tickSize(-this.width);
+
+ if (this.y_g != null) this.y_g.remove();
+ this.y_g = this.axis_g.append("g").call(this.y_axis);
+
+ // Hide the first tick use to prevent data from being placed on top of the chart
+ this.y_g.select(".tick:first-of-type").attr("opacity", "0");
+
+ // Set repo_name class
+ this.y_g
+ .selectAll(".tick")
+ .selectAll("text")
+ .call((g) => g.classed("repo_name", true))
+ .on("click", (e, d: number) => {
+ this.emotionService.selection = repositories[d - 1].getDisplayName();
+ this.router.navigate(["personal"]);
});
- this.chartData = chartData;
- this.chartOptions.scales.yAxes[0].labels = labels;
- }
- refreshGraph() {
- this.myChart.chart.destroy();
- this.myChart.ngOnInit();
- this.selectZoom(this.drag);
- this.updateZoom();
+ // Use custom domain
+ this.axis_abs_g.selectAll(".domain").style("opacity", "0");
+ this.axis_g.selectAll(".domain").style("opacity", "0");
+
+ this.axis_g
+ .append("g")
+ .attr("class", "axis")
+ .append("line")
+ .attr("x1", 0)
+ .attr("x2", 0)
+ .attr("y1", 0)
+ .attr("y2", this.scrollable_height);
+
+ this.axis_g
+ .append("g")
+ .attr("class", "axis")
+ .append("line")
+ .attr("x1", 0)
+ .attr("x2", this.width)
+ .attr("y1", this.scrollable_height)
+ .attr("y2", this.scrollable_height);
}
- onChartClick(event) {
- if (event.active.length > 0) {
- const data = this.getDataFromChart(event);
- window.open(data.commit.url, "_blank");
+ getCommitGroupPathD(first: Commit, last: Commit) {
+ let begin_x = this.xScaledTimeZoned(first.commitDate);
+ let end_x = this.xScaledTimeZoned(last.commitDate);
+
+ if (last.isCloture) {
+ return `M 0 0 h ${Math.max(
+ end_x - begin_x,
+ 1.5 * OverviewComponent.CIRCLE_RADIUS
+ )} a ${OverviewComponent.CIRCLE_RADIUS} ${OverviewComponent.CIRCLE_RADIUS
+ } 0 0 1 0 ${OverviewComponent.GROUP_HEIGHT} H 0 z`;
} else {
- const rawDate = this.getDateFromEvent(event.event);
- setTimeout(() => {
- this.openContextMenu(event.event.pageX, event.event.pageY, rawDate);
- });
+ return `M 0 0 h ${Math.max(
+ end_x - begin_x,
+ 1.5 * OverviewComponent.CIRCLE_RADIUS
+ )} v ${OverviewComponent.CIRCLE_RADIUS} H 0 z`;
+ }
+ }
+
+ getCommitGroupComponentFromScratch(
+ parent: d3.Selection,
+ commits: Commit[]
+ ): d3.Selection {
+ let sorted = commits.sort(
+ (a, b) => a.commitDate.getTime() - b.commitDate.getTime()
+ );
+
+ let g = parent.append("g").datum(sorted);
+
+ let begin_x = this.xScaledTimeZoned(sorted[0].commitDate);
+ let end_x = this.xScaledTimeZoned(sorted[sorted.length - 1].commitDate);
+
+ g.attr("class", "commit-group commit")
+ .append("path")
+ .attr("d", this.getCommitGroupPathD(sorted[0], sorted[sorted.length - 1]))
+ .attr("transform", `translate(0, ${-OverviewComponent.GROUP_HEIGHT / 2})`)
+ .attr("fill", sorted[sorted.length - 1].color.color)
+ .attr("class", "data");
+
+ let range = 0;
+ for (let i = 0; i < commits.length - 1; i++) {
+ range = Math.max(
+ range,
+ commits[i + 1].commitDate.getTime() - commits[i].commitDate.getTime()
+ );
}
+
+ g.attr("group_range", range);
+ g.attr("transform", `translate(${begin_x}, 0)`)
+ .on("mouseenter", (e, d) => {
+ this.hovered_commit = undefined;
+ this.hovered_group_commit = d;
+ this.hovered_g = g;
+ })
+ .on("mouseleave", () => {
+ if (this.hovered_g === g) {
+ this.hovered_g = undefined;
+ this.hovered_group_commit = undefined;
+ }
+ });
+
+ return g;
}
- getDateFromEvent(event) {
- const xAxis = this.myChart.chart.scales["x-axis-0"];
- const x = event.offsetX;
- return xAxis.getValueForPixel(x);
+ getCommitGroupComponent(
+ parent: d3.Selection,
+ group: d3.Selection | undefined,
+ commit: Commit
+ ): d3.Selection {
+ let g;
+
+ if (group == null) {
+ let x = this.xScaledTimeZoned(commit.commitDate);
+
+ g = parent.append("g").datum([commit]);
+
+ g.attr("class", "commit-group")
+ .append("path")
+ .attr("d", this.getCommitGroupPathD(commit, commit))
+ .attr(
+ "transform",
+ `translate(0, ${-OverviewComponent.GROUP_HEIGHT / 2})`
+ )
+ .attr("fill", commit.color.color)
+ .attr("class", "data")
+ .on("mouseenter", (e, d) => (this.hovered_group_commit = d))
+ .on("mouseleave", () => {
+ this.hovered_group_commit = undefined;
+ });
+
+ g.attr("transform", `translate(${x}, 0)`);
+ } else {
+ if (group.select("path").empty()) {
+ let group_commit = group.datum()[0];
+ let before_date = group.attr("before_date");
+ let after_date = group.attr("after_date");
+ group.remove();
+ group = this.getCommitGroupComponent(parent, undefined, group_commit);
+ group.attr("before_date", before_date);
+ group.attr("after_date", after_date);
+ }
+
+ g = group;
+ let all_commits = group
+ .datum()
+ .concat(commit)
+ .sort((a, b) => a.commitDate.getTime() - b.commitDate.getTime());
+ let spacing = Number.MAX_VALUE;
+ let j = all_commits.indexOf(commit);
+ if (j < all_commits.length - 1)
+ spacing = Math.min(
+ Math.abs(
+ all_commits[j + 1].commitDate.getTime() -
+ commit.commitDate.getTime()
+ ),
+ spacing
+ );
+ if (j > 0)
+ spacing = Math.min(
+ Math.abs(
+ all_commits[j - 1].commitDate.getTime() -
+ commit.commitDate.getTime()
+ ),
+ spacing
+ );
+
+ group.datum(all_commits);
+
+ let begin_x = this.xScaledTimeZoned(all_commits[0].commitDate);
+ let end_x = this.xScaledTimeZoned(
+ all_commits[all_commits.length - 1].commitDate
+ );
+
+ g.select("path")
+ .attr(
+ "d",
+ this.getCommitGroupPathD(
+ all_commits[0],
+ all_commits[all_commits.length - 1]
+ )
+ )
+ .attr("fill", all_commits[all_commits.length - 1].color.color);
+
+ g.attr("group_range", Math.max(spacing, g.attr("group_range") || 0));
+ g.attr("transform", `translate(${begin_x}, 0)`);
+ }
+
+ return g;
}
- onChartHover(event) {
- const data = this.getDataFromChart(event);
+ getCommitSimpleComponent(
+ parent: d3.Selection,
+ commit: Commit
+ ): d3.Selection {
+ let g = parent.append("g").datum([commit]);
+
+ g.classed("simple-commit", true);
+
+ let x = this.xScaledTimeZoned(commit.commitDate);
+
+ let comp: d3.Selection = g
+ .append("a")
+ .attr("href", (d) => d[0].url)
+ .attr("target", "_blank");
+
+ if (commit.isCloture) {
+ comp = comp.append("circle").attr("class", "commit-cloture");
+ } else {
+ comp = comp.append("rect").attr("class", "commit-normal");
+ }
+
+ comp.attr("fill", commit.color.color);
+ g.attr("date", (commit.commitDate as Date).getTime());
+
+ g.attr("transform", `translate(${x}, 0)`)
+ .on("mouseenter", () => {
+ this.hovered_commit = commit;
+ this.hovered_group_commit = undefined;
+ this.hovered_g = undefined;
+ })
+ .on("mouseleave", () => {
+ if (
+ this.hovered_commit &&
+ this.hovered_commit.commitDate === commit.commitDate
+ ) {
+ this.hovered_commit = undefined;
+ }
+ });
+
+ return g;
}
- selectUnit(unit: string) {
- this.unit = unit;
- this.myChart.chart.options.scales.xAxes[0].time.unit = unit;
- this.myChart.chart.update();
+ shouldGroupCommit(commit_before: Commit, commit_after: Commit): boolean {
+ return (
+ !commit_before.isCloture &&
+ this.xScaledTimeZoned(commit_after.commitDate) -
+ this.xScaledTimeZoned(commit_before.commitDate) <
+ Utils.COMMIT_FUSE_RANGE
+ );
}
- changeUnit() {
- if (this.unit === "week") {
- this.selectUnit("day");
- } else if (this.unit === "day") {
- this.selectUnit("hour");
- } else if (this.unit === "hour") {
- this.selectUnit("week");
+ getCommitComponent(
+ parent: d3.Selection,
+ commit: Commit,
+ before: d3.Selection
+ ): d3.Selection {
+ let should_be_grouped_with_last =
+ before != null &&
+ this.shouldGroupCommit(before.datum()[before.datum().length - 1], commit);
+
+ let g: d3.Selection;
+
+ const time = commit.commitDate.getTime();
+ if (!should_be_grouped_with_last) {
+ g = this.getCommitSimpleComponent(parent, commit);
+ if (before != null) {
+ g.attr("before_date", before.attr("end_date") || before.attr("date"));
+ }
+ g.attr("after_date", time);
+ } else {
+ g = this.getCommitGroupComponent(parent, before, commit);
+ g.attr("end_date", time);
}
+
+ g.classed("commit", true);
+
+ return g;
+ }
+
+ static formatDay = d3.utcFormat("%d/%m/%Y");
+ static formatHour = d3.utcFormat("%H:%M");
+
+ static multiFormat(spacing: number, date: Date) {
+ const options: Intl.NumberFormatOptions = {
+ useGrouping: false,
+ minimumIntegerDigits: 2,
+ };
+
+ if (spacing > 24 * 3600)
+ return `${date.getDate().toLocaleString(undefined, options)}/${(
+ date.getMonth() + 1
+ ).toLocaleString(undefined, options)}/${date
+ .getFullYear()
+ .toLocaleString(undefined, options)}`;
+ else
+ return `${date.getHours().toLocaleString(undefined, options)}:${date
+ .getMinutes()
+ .toLocaleString(undefined, options)}`;
}
- saveMinMaxZoom(min: number, max: number) {
- this.minZoom = min;
- this.maxZoom = max;
+ loadPoints() {
+ const overview = this;
+ const repositories: Repository[] = this.getDisplayedRepositories();
+
+ if (this.repository_g != null) this.repository_g.remove();
+
+ this.repository_g = this.data_g.append("g");
+ this.repositories_g = new Array(repositories.length);
+ let [minDate, maxDate] = d3.extent(
+ repositories.map((v) => v.commits).reduce((a, b) => a.concat(b), []),
+ (d) => d.commitDate
+ );
+ this.setupAxis(repositories, minDate, maxDate);
+
+ this.maxZoom = (maxDate.getTime() - minDate.getTime()) / (1000 * 60);
+
+ this.repository_g
+ .selectAll(".repository")
+ .data(repositories)
+ .enter()
+ .append("g")
+ .each(function (repository: Repository, i: number) {
+ overview.repositories_g[i] = d3.select(this);
+ overview.repositories_g[i].classed("repository", true);
+ overview.repositories_g[i]
+ .attr("repository_index", i)
+ .attr("transform", `translate(0, ${overview.y_scale(i + 1)})`);
+
+ let before = undefined;
+ let commits = repository.commits
+ .filter(
+ (commit) =>
+ !overview.searchFilter.length ||
+ overview.searchFilter.includes(commit.question)
+ )
+ .sort((a, b) => a.commitDate.getTime() - b.commitDate.getTime());
+
+ let minDateTime: number, maxDateTime: number;
+
+ let lines = [];
+ let current_line: Commit | undefined = undefined;
+
+ commits.forEach((commit) => {
+ minDateTime =
+ minDateTime == null
+ ? commit.commitDate.getTime()
+ : Math.min(commit.commitDate.getTime(), minDateTime);
+ maxDateTime =
+ minDateTime == null
+ ? commit.commitDate.getTime()
+ : Math.max(commit.commitDate.getTime(), minDateTime);
+ if (commit.message === "Resume") current_line = commit;
+ else if (commit.message === "Pause" && current_line) {
+ lines.push([current_line.commitDate, commit.commitDate]);
+ current_line = undefined;
+ }
+ before = overview.getCommitComponent(d3.select(this), commit, before);
+ });
+
+ if (lines.length === 0) {
+ lines.push([new Date(minDateTime), new Date(maxDateTime)]);
+ }
+
+ lines.forEach(([d1, d2]) => {
+ overview.repositories_g[i]
+ .insert("line", ":first-child")
+ .attr("class", "commit_line")
+ .attr("min_date", d1.getTime())
+ .attr("max_date", d2.getTime())
+ .attr("x1", overview.xScaledTimeZoned(d1))
+ .attr("x2", overview.xScaledTimeZoned(d2));
+ });
+ });
}
- setMinMaxZoom(min: number, max: number) {
- this.myChart.chart.options.scales.xAxes[0].ticks.min = min;
- this.myChart.chart.options.scales.xAxes[0].ticks.max = max;
- this.myChart.update();
+ refreshRepoBySplittingGroup(repo_g) {
+ const overview = this;
+ repo_g.selectAll(".commit-group:not(.hidden)").each(function () {
+ let g = d3.select(this);
+ let range = Number.parseInt(g.attr("group_range"));
+ let date = Number.parseInt(g.attr("after_date"));
+
+ let range_in_pixel =
+ overview.xScaledTimeZoned(new Date(range + date)) -
+ overview.xScaledTimeZoned(new Date(date));
+
+ if (range_in_pixel >= Utils.COMMIT_FUSE_RANGE) {
+ if (overview.hovered_g === g) {
+ overview.hovered_g = undefined;
+ overview.hovered_group_commit = undefined;
+ }
+ let before = undefined;
+ let commits = g.datum() as Commit[];
+ g.remove();
+ commits.forEach((commit) => {
+ before = overview.getCommitComponent(repo_g, commit, before);
+ });
+ }
+ });
}
- updateZoom() {
- this.setMinMaxZoom(this.minZoom, this.maxZoom);
+ refreshRepoByGrouping(repo_g) {
+ const overview = this;
+ let before = undefined;
+ let toCommit = [];
+ let toRemove = [];
+
+ repo_g
+ .selectAll(".commit:not(.hidden)")
+ .sort(
+ (a: Commit[], b: Commit[]) =>
+ a[0].commitDate.getTime() - b[0].commitDate.getTime()
+ )
+ .each(function (commit: Commit[]) {
+ let g: d3.Selection = d3.select(this);
+
+ if (before == null) {
+ before = g;
+ return;
+ }
+
+ let last_commit: Commit = before.datum()[before.datum().length - 1];
+ if (overview.shouldGroupCommit(last_commit, commit[0])) {
+ let commits = commit.concat(before.datum());
+ toRemove.push(before, g);
+
+ let before_date = before.attr("before_date");
+ let after_date = g.attr("after_date");
+ before = overview.getCommitGroupComponentFromScratch(repo_g, commits);
+ before.classed("commit", false);
+ toCommit.push(before);
+ before.attr("before_date", before_date);
+ before.attr("after_date", after_date);
+ } else before = g;
+ })
+ .sort(
+ (a: Commit[], b: Commit[]) =>
+ a[0].commitDate.getTime() - b[0].commitDate.getTime()
+ );
+
+ toRemove.forEach((g) => g.remove());
+ toCommit.forEach((g) => g.classed("commit", true));
}
- getPointStyle(commit: Commit) {
- if (commit.isCloture) {
- return "circle";
+ onBrush(event) {
+ // What are the selected boundaries?
+ let extent = event.selection;
+
+ // If no selection, back to initial coordinate. Otherwise, update X axis domain
+ if (!extent) {
+ this.x_scale_copy.domain([4, 8]);
} else {
- return "rectRot";
+ this.x_scale_copy.domain([
+ this.x_scale_copy.invert(extent[0]),
+ this.x_scale_copy.invert(extent[1]),
+ ]);
+ this.brush.clear(this.svg);
}
+
+ // Update axis and area position
+ this.x_g.transition().duration(1000).call(d3.axisBottom(this.x_scale_copy));
}
- getDataFromChart(event) {
- const datasetIndex = event.active[0]._datasetIndex;
- const dataIndex = event.active[0]._index;
- return this.chartData[datasetIndex].data[dataIndex];
+ getOffset(d: Date) {
+ return 0;
+ // return (
+ // this.x_scale_copy(d) -
+ // this.x_scale_copy(new Date(d.getTime() + d.getTimezoneOffset() * 60000))
+ // );
}
- resetZoom() {
- this.saveMinMaxZoom(null, null);
- this.updateZoom();
- this.adaptScaleWithChart(this.myChart.chart);
+ xScaledTimeZoned(d: Date) {
+ if (!d) {
+ return Number.MIN_VALUE;
+ }
+
+ return this.x_scale_copy(d) + this.getOffset(d);
}
- changeZoom() {
- let zoomOptions = this.myChart.chart.options.plugins.zoom.zoom;
+ refreshElementState() {
+ const containerRect = (d3.select(".chart-container") as any)
+ .node()
+ .getBoundingClientRect();
+ const overview = this;
- this.selectZoom(!zoomOptions.drag);
+ if (overview.repository_g)
+ overview.repositories_g.forEach((repo_g, i: number) => {
+ repo_g.selectAll(".commit").each(function () {
+ let g: d3.Selection = d3.select(this);
- this.myChart.chart.update();
- }
+ let node = repo_g.node();
+ let nodeRect = (node as any).getBoundingClientRect();
- selectZoom(drag: boolean) {
- let zoomOptions = this.myChart.chart.options.plugins.zoom.zoom;
- let panOptions = this.myChart.chart.options.plugins.zoom.pan;
+ const nodeVisible =
+ nodeRect.right >= containerRect.left &&
+ nodeRect.left <= containerRect.right &&
+ nodeRect.bottom >= containerRect.top &&
+ nodeRect.top <= containerRect.bottom;
- zoomOptions.drag = drag;
- this.drag = drag;
- panOptions.enabled = !drag;
- }
+ g.classed("hidden", !nodeVisible);
+ });
- searchSubmit() {
- this.loadGraphDataAndRefresh();
+ repo_g
+ .selectAll(".commit:not(.hidden)")
+ .attr(
+ "transform",
+ (commits: Commit[]) =>
+ `translate(${overview.xScaledTimeZoned(
+ commits[0].commitDate
+ )}, 0)`
+ );
+
+ repo_g
+ .selectAll("path:not(.hidden)")
+ .attr("d", (commits: Commit[]) =>
+ this.getCommitGroupPathD(commits[0], commits[commits.length - 1])
+ );
+ });
+
+ this.chart_abs_g.selectAll(".milestone").each(function (m: Milestone) {
+ let g = d3.select(this);
+ let x = overview.xScaledTimeZoned(m.date);
+ g.classed("hidden", x < 0 || x > overview.width);
+ });
+
+ overview.repositories_g.forEach((repo_g) =>
+ this.refreshRepoBySplittingGroup(repo_g)
+ );
+ overview.repositories_g.forEach((repo_g) =>
+ this.refreshRepoByGrouping(repo_g)
+ );
+
+ overview.repositories_g.forEach((g, i) => {
+ g.selectAll(".commit_line")
+ .attr("x1", function () {
+ let real_x = overview.xScaledTimeZoned(
+ new Date(Number.parseInt(d3.select(this).attr("min_date")))
+ );
+ return Math.max(Math.min(real_x, overview.width), 0);
+ })
+ .attr("x2", function () {
+ let real_x = overview.xScaledTimeZoned(
+ new Date(Number.parseInt(d3.select(this).attr("max_date")))
+ );
+ return Math.max(Math.min(real_x, overview.width), 0);
+ });
+ });
+
+ this.session_g
+ .selectAll(".session")
+ .attr("x", (s: Session) => {
+ return overview.xScaledTimeZoned(s.startDate);
+ })
+ .attr(
+ "width",
+ (s: Session) =>
+ overview.xScaledTimeZoned(s.endDate) -
+ overview.xScaledTimeZoned(s.startDate)
+ );
+
+ this.chart_abs_g
+ .selectAll(".milestone")
+ .attr(
+ "transform",
+ (m: Milestone) =>
+ `translate(${overview.xScaledTimeZoned(m.date)}, ${this.inner_margin.top})`
+ );
}
- adaptScaleWithChart(chart) {
- let min = new Date(chart.scales["x-axis-0"].min);
- let max = new Date(chart.scales["x-axis-0"].max);
- this.adaptScale(min, max);
+ toggleDrag() {
+ this.drag = !this.drag;
}
- adaptScale(min, max) {
- let distance = (max.getTime() - min.getTime()) / (1000 * 60 * 60 * 24);
+ resetZoom(conserve?: boolean) {
+ this.data_g
+ .transition()
+ .duration(750)
+ .call(
+ this.zoom.transform,
+ (conserve ? this.current_zoom : undefined) ||
+ d3.zoomIdentity.translate(0, 0).scale(1)
+ );
- if (this.unit === "day") {
- if (Math.round(distance) > 7) {
- this.selectUnit("week");
- } else if (Math.floor(distance) < 1) {
- this.selectUnit("hour");
- }
- } else if (this.unit === "week") {
- if (Math.round(distance) < 9) {
- this.selectUnit("day");
- }
- } else if (this.unit === "hour") {
- if (Math.round(distance) > 1) {
- this.selectUnit("day");
- }
- }
+ // this.svg.append("g").attr("class", "brush").call(this.brush);
+ }
+
+ searchSubmit() {
+ this.loadGraphDataAndRefresh();
}
openUploadFileModal() {
diff --git a/src/app/components/graphs/personal/personal.component.html b/src/app/components/graphs/personal/personal.component.html
new file mode 100644
index 0000000..4c87e71
--- /dev/null
+++ b/src/app/components/graphs/personal/personal.component.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/graphs/personal/personal.component.scss b/src/app/components/graphs/personal/personal.component.scss
new file mode 100644
index 0000000..999242e
--- /dev/null
+++ b/src/app/components/graphs/personal/personal.component.scss
@@ -0,0 +1,67 @@
+#form-control {
+ width: 100%;
+}
+
+#chart {
+ --top-inner: 20px;
+ --bottom-inner: 20px;
+ --left-inner: 50px;
+
+ --chart-height: 100%;
+
+ --dot-size: 5px;
+ --dot-size-incr: 2px;
+
+ min-height: min(calc(var(--top-inner) + var(--bottom-inner)), 100px);
+ max-height: var(--chart-height);
+
+ width: 100%;
+ height: 620px;
+ font-size: 2px;
+}
+
+#tooltip {
+ position: absolute;
+}
+
+.chart-container {
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ min-height: 0px;
+ max-height: 660px;
+ vertical-align: top;
+}
+
+.commit-normal {
+ stroke: #fff;
+ stroke-width: 2px;
+}
+
+.commit-normal:hover {
+ fill: white;
+ stroke: black;
+ }
+
+
+#commit_hover {
+ width: fit-content;
+ color: white;
+ background-color: rgba(0, 0, 0, 0.7);
+ font-size: 0;
+ padding: 5px;
+ border-radius: 5px;
+ transform: translateY(-90px);
+ user-select: none;
+}
+
+.commit_hover_element > p {
+ font-size: 10px;
+ margin: 2px;
+}
+
+.axis_title > p {
+ fill: black;
+ font-size: 10px;
+}
\ No newline at end of file
diff --git a/src/app/components/graphs/personal/personal.component.ts b/src/app/components/graphs/personal/personal.component.ts
new file mode 100644
index 0000000..67093ad
--- /dev/null
+++ b/src/app/components/graphs/personal/personal.component.ts
@@ -0,0 +1,299 @@
+import { Component, OnInit } from "@angular/core";
+import { BaseGraphComponent } from "../base-graph.component";
+import { DataService } from "../../../services/data.service";
+import { CommitsService } from "../../../services/commits.service";
+import { TranslateService } from "@ngx-translate/core";
+import { LoaderService } from "../../../services/loader.service";
+import { AssignmentsService } from "../../../services/assignments.service";
+import { Repository } from "../../../models/Repository.model";
+import { EmotionService } from "../../../services/emotion.service";
+
+import * as d3 from "d3";
+import { Utils } from "../../../services/utils";
+import { Commit } from "../../../models/Commit.model";
+
+@Component({
+ selector: "personal",
+ templateUrl: "./personal.component.html",
+ styleUrls: ["./personal.component.scss"],
+})
+export class PersonalComponent extends BaseGraphComponent implements OnInit {
+ svg: d3.Selection;
+ axis_g: any;
+ x_scale: d3.ScaleLinear;
+ inner_width: number;
+ x_axis: d3.Axis;
+ inner_height: any;
+ chart_svg: d3.Selection;
+ x_g: d3.Selection;
+ inner_margin: any;
+ y_scale: d3.ScaleLinear;
+ y_axis: d3.Axis;
+ width: any;
+ y_g: d3.Selection;
+ height: number;
+ chart_width: number;
+ data_g: d3.Selection;
+ dot_size: number;
+
+ hovered_commit: Commit[];
+ hovered_g: d3.Selection;
+
+ commit_date_format = Utils.COMMIT_DATE_FORMAT;
+
+ emotion_interval = [0, 7];
+ difficulty_interval = [0, 7];
+ dot_size_incr: number;
+
+ /**
+ * StudentsCommitsComponent constructor
+ * @param dataService Service used to store and get data
+ * @param translateService Service used to translate the application
+ */
+ constructor(
+ public emotionService: EmotionService,
+ public translateService: TranslateService,
+ protected loaderService: LoaderService,
+ protected assignmentsService: AssignmentsService
+ ) {
+ super(loaderService, assignmentsService, emotionService.dataService);
+ }
+
+ ngOnInit(): void {
+ if (!this.emotionService.selection && this.getRepositories().length > 0) {
+ this.emotionService.selection =
+ this.getRepositories()[0].getDisplayName();
+ }
+
+ setTimeout(() => {
+ this.translateService.onLangChange.subscribe(() => {
+ this.loadGraphDataAndRefresh();
+ });
+
+ if (this.dataService.repoToLoad) {
+ this.loadGraph();
+ } else {
+ this.loading = true;
+ this.loadGraphMetadata(
+ this.dataService.repositories,
+ this.dataService.reviews,
+ this.dataService.corrections,
+ this.dataService.questions
+ );
+ this.loading = false;
+ }
+ });
+ }
+
+ /**
+ * Updates dict variable with students data and loads graph labels which displays data on the graph
+ */
+ loadGraphDataAndRefresh() {
+ if (
+ !this.getRepositories()
+ .map((v) => v.getDisplayName())
+ .includes(this.emotionService.selection)
+ ) {
+ this.emotionService.selection =
+ this.getRepositories()[0]?.getDisplayName();
+ }
+
+ this.refresh();
+ }
+
+ updateVariableFromCss(): void {
+ let chart_div = document.getElementById("chart");
+
+ var style = getComputedStyle(chart_div);
+
+ var css_var_number = (name: string, dash = true) =>
+ Number.parseInt(style.getPropertyValue((dash ? "--" : "") + name));
+
+ let rect = chart_div.getBoundingClientRect();
+ this.width = rect.width;
+ this.height = rect.height;
+
+ this.inner_margin = {
+ top: css_var_number("top-inner"),
+ bottom: css_var_number("bottom-inner"),
+ left: css_var_number("left-inner"),
+ };
+
+ this.inner_width = this.width;
+ this.inner_height =
+ this.height - this.inner_margin.top - this.inner_margin.bottom;
+
+ this.dot_size = css_var_number("dot-size");
+ this.dot_size_incr = css_var_number("dot-size-incr");
+ }
+
+ setupAxis() {
+ if (this.axis_g != null) this.axis_g.remove();
+ const inner_min = Math.min(this.inner_height, this.inner_width);
+
+ this.x_scale = d3
+ .scaleLinear()
+ .domain(this.emotion_interval)
+ .range([0, inner_min])
+ .nice();
+
+ this.x_axis = d3
+ .axisBottom(this.x_scale)
+ .tickValues(
+ [...Array(this.emotion_interval[1] + 1).keys()].filter(
+ (v) => v >= this.emotion_interval[0]
+ )
+ );
+
+ this.axis_g = this.chart_svg.insert("g", ":first-child");
+
+ this.x_g = this.chart_svg
+ .append("g")
+ .attr("transform", "translate(" + [0, this.inner_height] + ")")
+ .call(this.x_axis);
+
+ this.chart_svg
+ .append("text") // text label for the x axis
+ .attr("x", inner_min / 2)
+ .attr("y", this.inner_height + 20)
+ .style("font-size", "10px")
+ .style("fill", "black")
+ .style("text-anchor", "middle")
+ .text(this.translateService.instant("PERSONAL-GRAPH.EMOTION"));
+
+ this.y_scale = d3.scaleLinear().domain([7, 0]).range([0, inner_min]).nice();
+
+ this.y_axis = d3
+ .axisLeft(this.y_scale)
+ .tickValues(
+ [...Array(this.difficulty_interval[1] + 1).keys()].filter(
+ (v) => v >= this.difficulty_interval[0]
+ )
+ );
+
+ if (this.y_g != null) this.y_g.remove();
+ this.y_g = this.axis_g.append("g").call(this.y_axis);
+
+ this.chart_svg
+ .append("text") // text label for the x axis
+ .attr("x", -this.inner_margin.left)
+ .attr("y", this.inner_height - inner_min / 2)
+ .style("font-size", "10px")
+ .style("fill", "black")
+ .style("text-anchor", "start")
+ .text(this.translateService.instant("PERSONAL-GRAPH.DIFFICULTY"));
+ }
+
+ refreshTooltip(x?: number, y?: number) {
+ if (x == null || y == null) {
+ return;
+ }
+ var tooltip = document.getElementById("tooltip");
+ if (tooltip == null) {
+ return;
+ }
+
+ tooltip.style.top = y + 20 + "px";
+ tooltip.style.left = x + 20 + "px";
+
+ if (this.hovered_g) {
+ if (this.hovered_g.select(":hover").empty()) {
+ this.hovered_commit = undefined;
+ this.hovered_g = null;
+ }
+ }
+ }
+
+ refresh() {
+ this.updateVariableFromCss();
+
+ const personal = this;
+
+ d3.select(".chart-container").on("mousemove", function (e) {
+ personal.refreshTooltip(e.clientX, e.clientY);
+ });
+
+ if (this.svg != null) this.svg.remove();
+
+ this.svg = d3
+ .select(".chart-container")
+ .append("svg")
+ .attr("preserveAspectRatio", "none")
+ .attr("viewBox", `0 0 ${this.width} ${this.height}`);
+
+ const translation = [this.inner_margin.left, this.inner_margin.top];
+
+ this.chart_svg = this.svg
+ .append("g")
+ .attr("transform", "translate(" + translation + ")");
+
+ this.setupAxis();
+ this.loadPoints();
+ }
+
+ loadPoints() {
+ let repo = this.emotionService.getSelection();
+ let data = this.emotionService.getData(repo);
+
+ if (this.data_g != null) this.data_g.remove();
+ if (repo == null || data == null || data.length == 0) return;
+ this.data_g = this.chart_svg.append("g");
+
+ const personal = this;
+
+ const grid: any[][][] = [...Array(this.emotion_interval[1] + 1).keys()].map(
+ (v) => [...Array(this.difficulty_interval[1] + 1).keys()].map((v) => [])
+ );
+
+ data.forEach((v) => {
+ grid[v.emotion][v.difficulty].push(v);
+ });
+
+ grid.forEach((v, x) => {
+ v.forEach((all, y) => {
+ if (all.length == 0) return;
+ let g = this.data_g.append("g");
+ g.datum(all);
+ g.append("circle")
+ .attr("cx", `${personal.x_scale(x)}`)
+ .attr("cy", `${personal.y_scale(y)}`)
+ .attr(
+ "r",
+ personal.dot_size + personal.dot_size_incr * (all.length - 1)
+ )
+ .on("mouseenter", () => {
+ console.log(all);
+ personal.hovered_commit = all;
+ personal.hovered_g = g;
+ })
+ .on("mouseleave", () => {
+ if (personal.hovered_g === g) {
+ personal.hovered_g = undefined;
+ personal.hovered_commit = undefined;
+ }
+ });
+ });
+ });
+ }
+
+ loadGraph() {
+ this.loading = true;
+ this.loaderService.loadRepositories().subscribe(() => {
+ this.loadGraphMetadata(
+ this.dataService.repositories,
+ this.dataService.reviews,
+ this.dataService.corrections,
+ this.dataService.questions
+ );
+ this.loading = false;
+ });
+ }
+
+ getRepositories(): Repository[] {
+ return this.dataService.repositories.filter(
+ (repository) =>
+ !this.dataService.groupFilter ||
+ repository.tpGroup === this.dataService.groupFilter
+ );
+ }
+}
diff --git a/src/app/components/nav-layouts/app-nav-layout/app-nav-layout.component.html b/src/app/components/nav-layouts/app-nav-layout/app-nav-layout.component.html
index 2cb73c9..547dae5 100644
--- a/src/app/components/nav-layouts/app-nav-layout/app-nav-layout.component.html
+++ b/src/app/components/nav-layouts/app-nav-layout/app-nav-layout.component.html
@@ -20,6 +20,8 @@
'NAVBAR.STUDENTS-COMMITS' | translate }}
{{
'NAVBAR.QUESTIONS-COMPLETION' | translate }}
+ {{
+ 'NAVBAR.PERSONAL' | translate }}
diff --git a/src/app/models/Repository.model.ts b/src/app/models/Repository.model.ts
index 410f11a..c95d027 100644
--- a/src/app/models/Repository.model.ts
+++ b/src/app/models/Repository.model.ts
@@ -46,7 +46,7 @@ export class Repository {
static getFormattedName(
last_name: string,
first_name: string,
- max_length: number,
+ max_length: number
): string {
assert(max_length > 0);
@@ -83,6 +83,7 @@ export class Repository {
}
getDisplayName() {
+ if (this.name === this.getNameFromUrl()) return this.name;
let displayName = this.name || "";
if (displayName.length > Utils.OVERVIEW_NAME_LENGTH_LIMIT) {
let numberOfSpace = (displayName.match(/ /g) || []).length;
@@ -94,7 +95,7 @@ export class Repository {
displayName = Repository.getFormattedName(
firstName,
lastName,
- Utils.OVERVIEW_NAME_LENGTH_LIMIT,
+ Utils.OVERVIEW_NAME_LENGTH_LIMIT
);
} else {
let findLastName = displayName.match(/^([A-Z\-]+ )*/g);
@@ -102,7 +103,7 @@ export class Repository {
displayName = Repository.getFormattedName(
findLastName[0].trim(),
displayName.substring(findLastName[0].length),
- Utils.OVERVIEW_NAME_LENGTH_LIMIT,
+ Utils.OVERVIEW_NAME_LENGTH_LIMIT
);
} else {
let lastspace = displayName.lastIndexOf(" ");
@@ -114,7 +115,7 @@ export class Repository {
displayName = Repository.getFormattedName(
lastName,
firstName,
- Utils.OVERVIEW_NAME_LENGTH_LIMIT,
+ Utils.OVERVIEW_NAME_LENGTH_LIMIT
);
}
}
@@ -138,10 +139,7 @@ export class Repository {
}
export class Error {
- constructor(
- public type: ErrorType,
- public message = "",
- ) {}
+ constructor(public type: ErrorType, public message = "") {}
}
export const enum ErrorType {
diff --git a/src/app/services/emotion.service.ts b/src/app/services/emotion.service.ts
new file mode 100644
index 0000000..9b54153
--- /dev/null
+++ b/src/app/services/emotion.service.ts
@@ -0,0 +1,85 @@
+import { Injectable } from "@angular/core";
+import { DatabaseService } from "./database.service";
+import { Assignment } from "../models/Assignment.model";
+import { DataService } from "./data.service";
+import { Repository } from "../models/Repository.model";
+import { Commit } from "../models/Commit.model";
+
+@Injectable({
+ providedIn: "root",
+})
+export class EmotionService {
+ /**
+ * The filtered repository selected during the session
+ */
+ selection: string;
+
+ repository_by_name: Map<
+ string,
+ {
+ repository: Repository;
+ data: { difficulty: number; emotion: number; commit: Commit }[];
+ }
+ >;
+
+ constructor(public dataService: DataService) {}
+
+ getSelection(): Repository | undefined {
+ if (!this.repository_by_name) {
+ this.repository_by_name = new Map(
+ this.dataService.repositories.map((v) => [
+ v.getDisplayName(),
+ { repository: v, data: undefined },
+ ])
+ );
+ }
+
+ return this.repository_by_name.get(this.selection)?.repository;
+ }
+
+ private generateDataForRepository(repository: Repository) {
+ let obj = this.repository_by_name.get(repository.getDisplayName());
+
+ obj.data = obj.repository.commits
+ .filter(
+ (v) =>
+ v.message.startsWith("Fix") &&
+ v.message.indexOf("\nD=") != -1 &&
+ v.message.indexOf("\nE=") != -1
+ )
+ .map((v) => {
+ let raw_data = v.message
+ .substring(v.message.indexOf("\nD=") + "\nD=".length)
+ .replace("\nE=", " ")
+ .trim();
+
+ let message = v.message.substring(0, v.message.indexOf("\n"));
+
+ let split = raw_data.split(" ");
+
+ return {
+ difficulty: Number.parseInt(split[0]),
+ emotion: Number.parseInt(split[1]),
+ message: message,
+ commit: v,
+ };
+ });
+ }
+
+ getData(repository: Repository) {
+ let obj = this.repository_by_name.get(repository.getDisplayName());
+
+ if (obj.data == null) this.generateDataForRepository(repository);
+ return obj.data;
+ }
+
+ containsEmotionReport(repository: Repository): boolean {
+ let obj = this.repository_by_name.get(repository.getDisplayName());
+
+ if (obj.data != null) return obj.data.length > 0;
+
+ this.generateDataForRepository(repository);
+
+ return obj.data.length > 0;
+ }
+}
diff --git a/src/app/services/utils.ts b/src/app/services/utils.ts
index 8c44b6c..951e7f0 100644
--- a/src/app/services/utils.ts
+++ b/src/app/services/utils.ts
@@ -13,6 +13,22 @@ export class Utils {
static readonly SLIDER_STEP = 86400000;
static readonly OVERVIEW_NAME_LENGTH_LIMIT = 20;
+ static readonly COMMIT_FUSE_RANGE = 5;
+ static readonly COMMIT_DATE_FORMAT = (date: Date) => {
+ const options: Intl.NumberFormatOptions = {
+ useGrouping: false,
+ minimumIntegerDigits: 2,
+ };
+
+ let year = date.getFullYear().toLocaleString(undefined, options);
+ let month = date.getMonth().toLocaleString(undefined, options);
+ let day = date.getDate().toLocaleString(undefined, options);
+ let hour = date.getHours().toLocaleString(undefined, options);
+ let minute = date.getMinutes().toLocaleString(undefined, options);
+ let seconds = date.getSeconds().toLocaleString(undefined, options);
+
+ return `${day}/${month}/${year} ${hour}:${minute}:${seconds}`;
+ };
static getTimeFromDate(date: Date) {
return date
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 9398f97..fe874f3 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -26,6 +26,7 @@
"OVERVIEW": "Overview",
"STUDENTS-COMMITS": "Students commits",
"QUESTIONS-COMPLETION": "Questions completion",
+ "PERSONAL": "Personal",
"METADATA": "Metadata",
"SIGN-IN": "Sign in",
"SIGN-OUT": "Sign out",
@@ -153,10 +154,19 @@
"CONFIRM": "Confirm",
"QUESTIONS": "Questions"
},
- "REREAD-TOOLTIP": "Reread"
+ "REREAD-TOOLTIP": "Reread",
+ "COMMIT-TOOLTIP": {
+ "BEGIN": "Group begin",
+ "END": "Group end",
+ "COMMIT_NUMBER": "Number of commits in group"
+ }
},
"STUDENTS-COMMITS-GRAPH": {},
"QUESTIONS-COMPLETION-GRAPH": {},
+ "PERSONAL-GRAPH": {
+ "EMOTION": "Emotion",
+ "DIFFICULTY": "Difficulty"
+ },
"METADATA": {
"PAGE-TITLE": "Edit metadata",
"TITLE": "Title",
diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json
index 9446c89..5711209 100644
--- a/src/assets/i18n/fr.json
+++ b/src/assets/i18n/fr.json
@@ -25,6 +25,7 @@
"OVERVIEW": "Vue d'ensemble",
"STUDENTS-COMMITS": "Commits des étudiants",
"QUESTIONS-COMPLETION": "Avancement des questions",
+ "PERSONAL": "Personnel",
"METADATA": "Métadonnées",
"SIGN-IN": "S'identifier",
"SIGN-OUT": "Se déconnecter",
@@ -152,10 +153,24 @@
"DELETE": "Supprimer le jalon",
"CONFIRM": "Confirmer"
},
- "REREAD-TOOLTIP": "Relire"
+ "REREAD-TOOLTIP": "Relire",
+ "COMMIT-TOOLTIP": {
+ "BEGIN": "Début du groupe",
+ "END": "Fin du groupe",
+ "COMMIT_NUMBER": "Nombre de commits dans le groupe"
+ }
+ },
+ "COMMIT-TOOLTIP": {
+ "START": "Début du groupe",
+ "END": "Fin du groupe",
+ "COMMIT_NUMBER": "Nombre de commits dans le groupe"
},
"STUDENTS-COMMITS-GRAPH": {},
"QUESTIONS-COMPLETION-GRAPH": {},
+ "PERSONAL-GRAPH": {
+ "EMOTION": "Emotion",
+ "DIFFICULTY": "Difficulté"
+ },
"METADATA": {
"PAGE-TITLE": "Editer les métadonnées",
"TITLE": "Titre",
diff --git a/yarn.lock b/yarn.lock
index c4af465..3dc1f58 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2038,6 +2038,221 @@
dependencies:
moment "^2.10.2"
+"@types/d3-array@*":
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
+ integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
+
+"@types/d3-axis@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795"
+ integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-brush@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c"
+ integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-chord@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d"
+ integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==
+
+"@types/d3-color@*":
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
+ integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-contour@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231"
+ integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==
+ dependencies:
+ "@types/d3-array" "*"
+ "@types/geojson" "*"
+
+"@types/d3-delaunay@*":
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1"
+ integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==
+
+"@types/d3-dispatch@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7"
+ integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==
+
+"@types/d3-drag@*":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02"
+ integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-dsv@*":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17"
+ integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==
+
+"@types/d3-ease@*":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
+ integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
+
+"@types/d3-fetch@*":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980"
+ integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==
+ dependencies:
+ "@types/d3-dsv" "*"
+
+"@types/d3-force@*":
+ version "3.0.9"
+ resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.9.tgz#dd96ccefba4386fe4ff36b8e4ee4e120c21fcf29"
+ integrity sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==
+
+"@types/d3-format@*":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90"
+ integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==
+
+"@types/d3-geo@*":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440"
+ integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==
+ dependencies:
+ "@types/geojson" "*"
+
+"@types/d3-hierarchy@*":
+ version "3.1.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b"
+ integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==
+
+"@types/d3-interpolate@*":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
+ integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+ dependencies:
+ "@types/d3-color" "*"
+
+"@types/d3-path@*":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a"
+ integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==
+
+"@types/d3-polygon@*":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c"
+ integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==
+
+"@types/d3-quadtree@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f"
+ integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==
+
+"@types/d3-random@*":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb"
+ integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==
+
+"@types/d3-scale-chromatic@*":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz#fc0db9c10e789c351f4c42d96f31f2e4df8f5644"
+ integrity sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==
+
+"@types/d3-scale@*":
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb"
+ integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==
+ dependencies:
+ "@types/d3-time" "*"
+
+"@types/d3-selection@*":
+ version "3.0.10"
+ resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe"
+ integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==
+
+"@types/d3-shape@*":
+ version "3.1.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72"
+ integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==
+ dependencies:
+ "@types/d3-path" "*"
+
+"@types/d3-time-format@*":
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2"
+ integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==
+
+"@types/d3-time@*":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be"
+ integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==
+
+"@types/d3-timer@*":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
+ integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+
+"@types/d3-transition@*":
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.8.tgz#677707f5eed5b24c66a1918cde05963021351a8f"
+ integrity sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-zoom@*":
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b"
+ integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
+ dependencies:
+ "@types/d3-interpolate" "*"
+ "@types/d3-selection" "*"
+
+"@types/d3@^7.4.3":
+ version "7.4.3"
+ resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2"
+ integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==
+ dependencies:
+ "@types/d3-array" "*"
+ "@types/d3-axis" "*"
+ "@types/d3-brush" "*"
+ "@types/d3-chord" "*"
+ "@types/d3-color" "*"
+ "@types/d3-contour" "*"
+ "@types/d3-delaunay" "*"
+ "@types/d3-dispatch" "*"
+ "@types/d3-drag" "*"
+ "@types/d3-dsv" "*"
+ "@types/d3-ease" "*"
+ "@types/d3-fetch" "*"
+ "@types/d3-force" "*"
+ "@types/d3-format" "*"
+ "@types/d3-geo" "*"
+ "@types/d3-hierarchy" "*"
+ "@types/d3-interpolate" "*"
+ "@types/d3-path" "*"
+ "@types/d3-polygon" "*"
+ "@types/d3-quadtree" "*"
+ "@types/d3-random" "*"
+ "@types/d3-scale" "*"
+ "@types/d3-scale-chromatic" "*"
+ "@types/d3-selection" "*"
+ "@types/d3-shape" "*"
+ "@types/d3-time" "*"
+ "@types/d3-time-format" "*"
+ "@types/d3-timer" "*"
+ "@types/d3-transition" "*"
+ "@types/d3-zoom" "*"
+
+"@types/geojson@*":
+ version "7946.0.14"
+ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613"
+ integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==
+
"@types/glob@^7.1.1":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
@@ -3869,6 +4084,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
+commander@7:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+ integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
commander@^2.11.0, commander@^2.19.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -4388,6 +4608,250 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==
+"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
+ integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+ dependencies:
+ internmap "1 - 2"
+
+d3-axis@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
+ integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
+
+d3-brush@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
+ integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-drag "2 - 3"
+ d3-interpolate "1 - 3"
+ d3-selection "3"
+ d3-transition "3"
+
+d3-chord@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
+ integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
+ dependencies:
+ d3-path "1 - 3"
+
+"d3-color@1 - 3", d3-color@3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-contour@4:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc"
+ integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
+ dependencies:
+ d3-array "^3.2.0"
+
+d3-delaunay@6:
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b"
+ integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
+ dependencies:
+ delaunator "5"
+
+"d3-dispatch@1 - 3", d3-dispatch@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+ integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+"d3-drag@2 - 3", d3-drag@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
+ integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-selection "3"
+
+"d3-dsv@1 - 3", d3-dsv@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
+ integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
+ dependencies:
+ commander "7"
+ iconv-lite "0.6"
+ rw "1"
+
+"d3-ease@1 - 3", d3-ease@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+d3-fetch@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
+ integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
+ dependencies:
+ d3-dsv "1 - 3"
+
+d3-force@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
+ integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-quadtree "1 - 3"
+ d3-timer "1 - 3"
+
+"d3-format@1 - 3", d3-format@3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+ integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+d3-geo@3:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.1.tgz#6027cf51246f9b2ebd64f99e01dc7c3364033a4d"
+ integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==
+ dependencies:
+ d3-array "2.5.0 - 3"
+
+d3-hierarchy@3:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
+ integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
+
+"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
+"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+ integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-polygon@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
+ integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
+
+"d3-quadtree@1 - 3", d3-quadtree@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
+ integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
+
+d3-random@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
+ integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
+
+d3-scale-chromatic@3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#34c39da298b23c20e02f1a4b239bd0f22e7f1314"
+ integrity sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==
+ dependencies:
+ d3-color "1 - 3"
+ d3-interpolate "1 - 3"
+
+d3-scale@4:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+ integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+ dependencies:
+ d3-array "2.10.0 - 3"
+ d3-format "1 - 3"
+ d3-interpolate "1.2.0 - 3"
+ d3-time "2.1.1 - 3"
+ d3-time-format "2 - 4"
+
+"d3-selection@2 - 3", d3-selection@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
+ integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
+d3-shape@3:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+ integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+ dependencies:
+ d3-path "^3.1.0"
+
+"d3-time-format@2 - 4", d3-time-format@4:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+ integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+ dependencies:
+ d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+ integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+ dependencies:
+ d3-array "2 - 3"
+
+"d3-timer@1 - 3", d3-timer@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+"d3-transition@2 - 3", d3-transition@3:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
+ integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
+ dependencies:
+ d3-color "1 - 3"
+ d3-dispatch "1 - 3"
+ d3-ease "1 - 3"
+ d3-interpolate "1 - 3"
+ d3-timer "1 - 3"
+
+d3-zoom@3:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
+ integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-drag "2 - 3"
+ d3-interpolate "1 - 3"
+ d3-selection "2 - 3"
+ d3-transition "2 - 3"
+
+d3@^7.9.0:
+ version "7.9.0"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d"
+ integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==
+ dependencies:
+ d3-array "3"
+ d3-axis "3"
+ d3-brush "3"
+ d3-chord "3"
+ d3-color "3"
+ d3-contour "4"
+ d3-delaunay "6"
+ d3-dispatch "3"
+ d3-drag "3"
+ d3-dsv "3"
+ d3-ease "3"
+ d3-fetch "3"
+ d3-force "3"
+ d3-format "3"
+ d3-geo "3"
+ d3-hierarchy "3"
+ d3-interpolate "3"
+ d3-path "3"
+ d3-polygon "3"
+ d3-quadtree "3"
+ d3-random "3"
+ d3-scale "4"
+ d3-scale-chromatic "3"
+ d3-selection "3"
+ d3-shape "3"
+ d3-time "3"
+ d3-time-format "4"
+ d3-timer "3"
+ d3-transition "3"
+ d3-zoom "3"
+
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
@@ -4570,6 +5034,13 @@ del@^4.1.1:
pify "^4.0.1"
rimraf "^2.6.3"
+delaunator@5:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278"
+ integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==
+ dependencies:
+ robust-predicates "^3.0.2"
+
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -6482,7 +6953,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
-iconv-lite@^0.6.2, iconv-lite@^0.6.3:
+iconv-lite@0.6, iconv-lite@^0.6.2, iconv-lite@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@@ -6661,6 +7132,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -10056,6 +10532,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
+robust-predicates@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
+ integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
+
rollup@2.32.1:
version "2.32.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.32.1.tgz#625a92c54f5b4d28ada12d618641491d4dbb548c"
@@ -10082,6 +10563,11 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
+rw@1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+ integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
+
rxjs-compat@^6.5.5:
version "6.6.7"
resolved "https://registry.yarnpkg.com/rxjs-compat/-/rxjs-compat-6.6.7.tgz#6eb4ef75c0a58ea672854a701ccc8d49f41e69cb"