Skip to content
Snippets Groups Projects
Commit 08a846df authored by Asheesh Gulati's avatar Asheesh Gulati
Browse files

GHP-142 Validation initiale

parents
Branches main
Tags v1.0.0
No related merge requests found
Showing
with 1855 additions and 0 deletions
.idea/
dist/
node_modules/
README.md 0 → 100644
# OLM Charts
OLM Charts is a free and open-source JavaScript library for visualizing [COMPER](https://comper.fr/en/) Open Learner Model profiles.
## Getting Started
### Dependencies
The project uses [D3](https://d3js.org/) and [requireJS](https://requirejs.org/), as well
as [Bootstrap](https://getbootstrap.com/) and [OverlayScrollbars](https://github.com/KingSora/OverlayScrollbars) for its
demos.
### Installation
Execute the following commands to bundle the library:
```shell
npm install
npm run build
```
## Usage
Once you include the resulting `olm.bundle.js`, the module will be available as `document._OLM`.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript" src="./olm.bundle.js"></script>
<title>OLM Visualization</title>
</head>
<body>
</body>
<script>
(function () {
const OLM = document._OLM;
// This creates a random COMPER profile for testing purposes.
const framework = OLM.CORE.Utils.getScoredFrameworkSample();
let fw_tree = new OLM.CORE.FrameworkTree();
fw_tree.buildFromFramework(framework);
// You can then instantiate the desired visualization.
})();
</script>
</html>
```
### Visualizations
A demo for each visualization can be found under the `./test` folder, as well
as [online](https://lachand.gitlab.io/open-learner-model/).
#### Tree Indented
To instantiate this visualization, the following configuration object (presented here with default values) must be
provided to the constructor:
```json
{
"fontHoverColor": "rgba(255, 255, 255, 1)",
"fontColor": "rgba(255, 255, 255, .85)",
"colors": [
{
"to": 0.25,
"color": "#cf000f"
},
{
"to": 0.5,
"color": "#f57f17"
},
{
"to": 0.75,
"color": "#ffee58"
},
{
"color": "#4caf50"
}
],
"showMastery": true,
"showTrust": true,
"showCover": true,
"formatMastery": "percentage",
"formatTrust": "percentage",
"formatCover": "percentage"
}
```
Available values for `formatMastery` are `percentage` and `2decimal`.
```javascript
const treeIndented = new OLM.TreeIndented(document.getElementById('test'), fw_tree, config);
treeIndented.onMouseOver = (node) => {
//
}
treeIndented.draw(svgId = 'test-indented');
// If svgId is omitted, a unique ID will be automatically created.
```
#### Tree Pack / Tree Partition / Tree Sunburst
To instantiate these visualizations, the following configuration object (presented here with default values) must be
provided to the constructor:
```json
{
"fontColor": "rgba(255, 255, 255, .85)",
"backgroundColor": "#343a40",
"formatMastery": "percentage",
"formatTrust": "percentage",
"formatCover": "percentage",
"useHash": true,
"hashTreshold": 0.1,
"useLegend": true,
"colors": [
{
"to": 0.25,
"color": "#cf000f"
},
{
"to": 0.5,
"color": "#f57f17"
},
{
"to": 0.75,
"color": "#ffee58"
},
{
"color": "#4caf50"
}
],
"noValueColor": "#808080"
}
```
Available values for `formatMastery` are `percentage` and `2decimal`. Moreover, `backgroundColor` only changes the
background color of the information overlay.
```javascript
const treePack = new OLM.TreePack(document.getElementById('test'), fw_tree, config);
treePack.onMouseOver = (node) => {
//
}
treePack.draw(svgId = 'test-pack');
// If svgId is omitted, a unique ID will be automatically created.
```
## Acknowledgments
Thanks to [@stopyransky](https://github.com/stopyransky) for
the [D3 Hierarchy Layouts](https://codepen.io/stopyransky/pen/EXdrOo).
This diff is collapsed.
{
"name": "olm-charts",
"version": "0.1.1",
"description": "Visualizations for the COMPER Open Learner Model module",
"main": "",
"directories": {},
"dependencies": {
"@types/d3": "5.9.2",
"@types/jquery": "3.5.1",
"bootstrap": "^5.1.3",
"d3": "^6.2.0",
"jquery": "^3.6.0",
"popper.js": "^1.16.1",
"requirejs": "^2.3.6",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.15.4",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"css-loader": "^6.5.1",
"style-loader": "^3.3.1",
"webpack": "^5.64.1",
"webpack-cli": "^4.9.1"
},
"scripts": {
"build": "webpack"
},
"repository": {
"type": "git",
"url": "https://gitlab.liris.cnrs.fr/comper/olm-charts.git"
},
"author": "Rémi Casado",
"contributors": [
{
"name": "Valentin Lachand",
"url": "https://valentin.lachand.net/"
}
],
"license": "ISC",
"homepage": "https://gitlab.liris.cnrs.fr/comper/olm-charts#olm-charts"
}
interface JsTree {
settings: any;
refresh(): void;
}
interface JQuery {
jstree(any: any): JsTree;
}
import {Utils} from "./utils";
import {FrameworkTree} from "./framework";
export class Core {
public static Utils = Utils;
public static FrameworkTree = FrameworkTree;
}
import {Utils} from "./utils";
interface FrameworkNodeRelation {
hasSkill: Array<string>,
isSkillOf: Array<string>,
hasKnowledge: Array<string>,
isKnowledgeOf: Array<string>,
composes: Array<string>,
isComposedOf: Array<string>,
complexifies?: Array<string>,
isComplexificationOf?: Array<string>,
isLeverOfUnderstandingOf?: Array<string>,
facilitatesUnderstandingOf?: Array<string>,
comprises?: Array<string>,
isComprisedIn?: Array<string>,
hasLearning?: Array<string>,
hasTraining?: Array<string>,
requires?: Array<string>,
isRequiredBy?: Array<string>,
[key: string]: Array<string>
}
interface FrameworkNode {
name: string,
type: string,
children?: Array<string>,
parents?: Array<string>,
cover?: number,
mastery?: number,
trust?: number,
resourcesCompleted?: number,
relations?: FrameworkNodeRelation,
resources?: Array<string>
[key: string]: (string | FrameworkNodeRelation | Array<string> | number)
}
interface FrameworkRoot {
name: string,
type: string
}
interface Framework {
name: string,
objects: Array<FrameworkNode>
resources: Array<FrameworkResources>
}
interface FrameworkTreeNode {
id: string, // Unique ID
data: FrameworkNode, // The node data
parent: string, // ID of the parent
children: Array<string>, // IDs of the children
depth: number, // depth of the node
resources: Array<FrameworkResources>,
[key: string]: (string | FrameworkNode | Array<string> | number | Array<FrameworkResources>);
}
interface FrameworkResources {
id: string,
name: string,
interactivityType: string,
learningResourceType: string,
significanceLevel: number,
difficulty: number,
typicalLearningTime: number,
learningPlatform: string
location: string,
author: string,
language: string,
generative: boolean
//[key :string] :(string | FrameworkNode | Array<string> | number);
}
class FrameworkTree {
/**
* Root of framework tree
*/
private root: FrameworkTreeNode = null;
/**
* Map of the framework nodes tree with their own id.
*/
private nodes: Map<string, FrameworkTreeNode> = new Map<string, FrameworkTreeNode>();
consextructor() {
}
static copy(source: FrameworkTree): FrameworkTree {
let copy = new FrameworkTree();
copy.nodes = new Map(source.nodes);
copy.root = source.root;
return copy;
}
/**
* Gets the parent node at the given depth.
* @param node
* @param depth
*/
public getNodeParentByDepth(node: FrameworkTreeNode, depth: number): FrameworkTreeNode {
while (node.depth > depth) {
node = this.nodes.get(node.parent);
}
return node;
}
/**
* Builds the tree from a framework.
* @param framework
*/
public buildFromFramework(framework: Framework): void {
this.root = {
"id": Utils.makeId(),
"data": {
"name": framework.name,
"type": "framework"
},
"parent": null,
"children": [],
"depth": 0,
"resources": [],
}
this.nodes.set(this.root.id, this.root);
// Copy of the framework to edit it without editing the source
let fw: Array<FrameworkNode> = Array.from(framework.objects);
// Increment var for the while loop below
let i: number = 0;
// Map <name, id>. Helps retrieve the FrameworkTreeNode.id from the corresponding node.name.
let createdNodes: Map<string, string> = new Map<string, string>();
// Creates a new FrameworkTreeNode for each node<->parentNode link.
// Once every node<->parentNode link as been treated, removes the node from fw.
// Loops until there are no nodes in fw anymore.
while (fw.length > 0) {
// "increment" will be False if we removed an element for fw, because 'i' will be on the new current node position.
let increment: boolean = true;
let node: FrameworkNode = fw[i];
let parents: Array<string> = [node.relations.isSkillOf, node.relations.isKnowledgeOf, node.relations.composes].flat();
const useFilter = (arr: any[]) => {
return arr.filter((value, index, self) => {
return self.indexOf(value) === index;
});
};
let resources = [node.relations.hasTraining, node.relations.hasLearning].flat();
let resourcesF: Array<FrameworkResources> = [];
for (let i = 0; i < resources.length; i++) {
for (let resourceFramework of framework.resources) {
if (resources[i] === resourceFramework["id"]) {
resourcesF[i] = resourceFramework;
}
}
}
parents = useFilter(parents);
// For the first level nodes, we simply link them to the root of the tree.
if (parents.length === 0) {
let id: string = Utils.makeId();
let treeNode: FrameworkTreeNode = {
"id": id,
"data": node,
"parent": this.root.id,
"children": [],
"depth": 1,
"resources": resourcesF,
}
this.nodes.set(id, treeNode);
this.root.children.push(id);
createdNodes.set(node.name, id);
fw.splice(i, 1);
increment = false;
} else {
// Adds the "linkedParents" names to the node, so if we have to loop again on the node, we know which
// node<->parentNode link has been traited or not.
if (node['linkedParents'] === undefined) node['linkedParents'] = [];
parents.forEach((parentName: string) => {
if (!(node['linkedParents'] as Array<string>).includes(parentName)) {
let parentId: string = createdNodes.get(parentName);
if (parentId !== undefined) {
let resources = [node.relations.hasTraining, node.relations.hasLearning].flat();
let resourcesF: Array<FrameworkResources> = [];
for (let i = 0; i < resources.length; i++) {
for (let resourceFramework of framework.resources) {
if (resources[i] === resourceFramework["id"]) {
resourcesF[i] = resourceFramework;
}
}
}
let parent: FrameworkTreeNode = this.nodes.get(parentId);
let id: string = Utils.makeId()
let treeNode: FrameworkTreeNode = {
"id": id,
"data": node,
"parent": parentId,
"children": [],
"depth": parent.depth + 1,
"resources": resourcesF,
}
this.nodes.set(id, treeNode);
parent.children.push(id);
createdNodes.set(node.name, id);
(node['linkedParents'] as Array<string>).push(parentName);
}
}
});
// If we traited all the node<->parentNode links, we can remove the node from fw.
if ((node['linkedParents'] as Array<string>).length === parents.length) {
fw.splice(i, 1);
increment = false;
}
}
// Increments or resets until we're finished with the nodes.
if (increment) i += 1;
if (i >= fw.length) i = 0;
}
}
/**
* Gets root
* @returns root
*/
public getRoot(): FrameworkTreeNode {
return this.root;
}
/**
* Gets a framework tree node of the tree.
* @param id Id of the framework tree node.
* @returns node A framework tree node.
*/
public getNode(id: string): FrameworkTreeNode {
return this.nodes.get(id);
}
/**
* Returns the nodes of the tree as array.
* @returns
*/
public getNodes() {
return Array.from(this.nodes.values());
}
/**
* Returns the nearest common ancestor of two given nodes.
* @param node1
* @param node2
* @returns nearest common ancestor
*/
public getNearestCommonAncestor(node1: FrameworkTreeNode, node2: FrameworkTreeNode): FrameworkTreeNode {
while (node1.depth > node2.depth) {
node1 = this.getNode(node1.parent);
}
while (node2.depth > node1.depth) {
node2 = this.getNode(node2.parent);
}
while (node1.id !== node2.id) {
node1 = this.getNode(node1.parent);
node2 = this.getNode(node2.parent);
}
return node1;
}
public getNodesByDepth(depth: number): Array<FrameworkTreeNode> {
let treeNodes: Map<string, FrameworkTreeNode> = this.nodes;
let resultNodes: Array<FrameworkTreeNode> = [];
(function recurse(currentNode) {
if (currentNode.depth < depth) {
for (var i = 0, length = currentNode.children.length; i < length; i++) {
recurse(treeNodes.get(currentNode.children[i]));
}
}
if (currentNode.depth === depth) resultNodes.push(currentNode);
})(this.root);
return resultNodes;
}
/**
* Traverses the tree depth first, starting from origin. Calls callback at each node, with the node as parameter.
* @param callback
* @param origin Default : the root of the tree
*/
public traverseDeepFirst(callback: (node: FrameworkTreeNode) => void, origin: FrameworkTreeNode = this.root): void {
let nodes: Map<string, FrameworkTreeNode> = this.nodes;
(function recurse(currentNode) {
for (var i = 0, length = currentNode.children.length; i < length; i++) {
recurse(nodes.get(currentNode.children[i]));
}
callback(currentNode);
})(origin);
};
/**
* Finds nodes
* @param values Rest tuple of a value and a path. The path is a single string separated by a dot.
* @returns nodes An array of framework tree nodes.
* @example let nodes = tree.findNode([1, "cover"], ["some_name", "relations.requires"])
*/
public findNodes(...values: Array<[(string | number), string]>): Array<FrameworkTreeNode> {
function hasData(node: FrameworkTreeNode, values: Array<[(string | number), string]>): boolean {
if (node.data === null || node.data === undefined) return false;
for (let i = 0; i < values.length; i++) {
let value: (string | number) = values[i][0];
let path: Array<string> = values[i][1].split('.');
let data: (string | FrameworkNodeRelation | number | Array<(string | number)>) = node.data[path[0]];
if (data === undefined) return false;
// Follows the path.
for (let j = 1; j < path.length; j++) {
if ((data as FrameworkNodeRelation)[path[j]] === undefined) return false;
;
data = (data as FrameworkNodeRelation)[path[j]];
}
// Checks weither the value is in a list of values or equal to the value.
if ((Array.isArray(data) && !data.includes(value)) || (!Array.isArray(data) && data !== value)) {
return false;
}
}
return true;
}
let nodes: Array<FrameworkTreeNode> = [];
let fwNodes: Array<FrameworkTreeNode> = Array.from(this.nodes.values());
for (let j = 0; j < fwNodes.length; j++) {
let node: FrameworkTreeNode = fwNodes[j];
if (hasData(node, values)) nodes.push(node);
}
return nodes;
}
/**
* Gets the siblings of a framework tree node.
* @param id Id of the framework tree node.
* @returns siblings An array of framework tree node.
*/
public getSiblings(id: string): Array<FrameworkTreeNode> {
let node: FrameworkTreeNode = this.nodes.get(id);
let parent: FrameworkTreeNode = this.nodes.get(node.parent);
let siblings: Array<FrameworkTreeNode> = [];
parent.children.forEach((childId: string) => {
siblings.push(this.nodes.get(childId));
})
return siblings;
}
}
export {FrameworkNode, Framework, FrameworkTreeNode, FrameworkTree, FrameworkRoot};
This diff is collapsed.
.ui-rangeslider .ui-rangeslider-sliders {
position: relative;
overflow: visible;
height: 30px;
margin: 0 68px;
}
html > body .ui-rangeslider .ui-rangeslider-sliders .ui-slider-track:first-child {
height: 15px;
border-width: 1px;
}
.ui-rangeslider .ui-rangeslider-sliders .ui-slider-track {
position: absolute;
top: 6px;
right: 0;
left: 0;
margin: 0;
}
.ui-bar-a, .ui-page-theme-a .ui-bar-inherit, html .ui-bar-a .ui-bar-inherit, html .ui-body-a .ui-bar-inherit, html body .ui-group-theme-a .ui-bar-inherit {
background-color: #e9e9e9;
border-color: #ddd;
color: #333;
text-shadow: 0 1px 0 #eee;
font-weight: 700;
}
.ui-btn-corner-all, .ui-btn.ui-corner-all, .ui-slider-track.ui-corner-all, .ui-flipswitch.ui-corner-all, .ui-li-count {
-webkit-border-radius: .3125em;
border-radius: .3125em;
}
.ui-slider-track .ui-btn.ui-slider-handle {
font-size: .9em;
line-height: 30px;
}
.ui-slider-track .ui-btn.ui-slider-handle {
position: absolute;
z-index: 1;
margin-top: -7px !important;
width: 28px;
height: 28px;
margin: -15px 0 0 -15px;
outline: 0;
padding: 0;
}
.ui-btn.ui-slider-handle {
font-size: 16px;
margin: .5em 0;
padding: .7em 1em;
display: block;
position: relative;
text-align: center;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: #f2f2f2;
border-color: #ddd;
color: #333;
text-shadow: 0 1px 0 #f3f3f3;
color: #333;
border-radius: .3125em;
font-size: small;
margin-top: -7px !important;
text-decoration: none;
}
.ui-shadow {
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
-moz-box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
}
.ui-slider-handle {
border-radius: .3125em;
}
/*! jqm-multislider (C)Armin Junge 13-05-2018 */
$.widget("vertumnus.multislider",{options:{min:0,max:100,step:1,showValue:!0},_create:function(){let e=this.element.attr("min"),t=this.element.attr("max"),i=this.element.attr("step"),s=this.element.attr("data-show-value");void 0!==e&&(this.options.min=parseInt(e)),void 0!==t&&(this.options.max=parseInt(t)),void 0!==i&&(this.options.step=parseInt(i)),void 0!==s&&(this.options.showValue="true"===s),this.element.addClass("ui-rangeslider"),this._label=this.element.find("label").first().insertBefore(this.element),this._sliderContainer=$('<div class="ui-rangeslider-sliders" />').prependTo(this.element),this._sliders=[];let n=this;this.element.find("input").each(function(e){n._newSlider($(this),e)}).change({widget:this},this._change).parent().hide(),this._initSliders()},_setOption:function(e,t){switch(e){case"showValue":if("boolean"!=typeof t)return!1;this._showValue(t)}this._super(e,t)},_change:function(e){my=e.data.widget;let t=$(e.target),i=parseInt(t.attr("pos")),s=parseInt(i>0?my._sliders[i-1].slider.val():my.options.min),n=parseInt(i<my._sliders.length-1?my._sliders[i+1].slider.val():my.options.max+1),l=parseInt(t.val());l<=s&&t.val(s+my.options.step).slider("refresh"),l>=n&&t.val(n-my.options.step).slider("refresh"),my.options.showValue&&my._sliderContainer.find(`a[pos=${i}]`).html(t.val());let red=my._sliders[0].slider[0].value;let orange=my._sliders[1].slider[0].value;let yellow=my._sliders[2].slider[0].value;document.getElementById('slider-red').style.width = red+"%";document.getElementById('slider-orange').style.width = orange+"%";document.getElementById('slider-yellow').style.width = yellow+"%";my._trigger("change",e)},_newSlider:function(e,t){let i=e.attr({min:this.options.min,max:this.options.max,step:this.options.step,"data-show-value":this.options.showValue,pos:t}).slider(),s=$.data(i[0],"mobile-slider");return s.slider.appendTo(this._sliderContainer),s.handle.attr({pos:t}),this._sliders.push({slider:i,handle:s.handle}),i},_initSliders:function(){let e=(this.options.max-this.options.min)/(this._sliders.length+1),t=parseInt(this.options.min);this._sliders.forEach(function(i){i.slider.val(Math.round(t+=e)).slider("refresh"),this.options.showValue&&i.handle.html(i.slider.val())},this)},_showValue:function(e){this._sliders.forEach(function(t){t.handle.html(e?t.slider.val():"")})},values:function(){return this._sliders.map(e=>parseInt(e.slider.val()))},value:function(e,t){return e<0||e>=this._sliders.length?null:void 0===t?parseInt(this._sliders[e].slider.val()):(this._sliders[e].slider.val(t).change(),this)},count:function(e){if(void 0===e)return this._sliders.length;let t=this._sliders.length;if(e<this._sliders.length)for(;t>e;--t)this.decrease();else for(;t<e;++t)this.increase();return this},increase:function(){this._newSlider($('<input type="range" />').appendTo(this.element),this._sliders.length).change({widget:this},this._change).parent().hide(),this._initSliders(),this._trigger("change")},decrease:function(){this._sliders.pop(),this.element.find(".ui-slider-track:last").remove(),this.element.find("input:last").slider("destroy").remove(),this._initSliders(),this._trigger("change")}});
import {Core} from "./core/core"
import {TreeIndented} from "./tree_indented/tree_indented";
import {TreePack} from "./tree_pack/tree_pack";
import {TreePartition} from "./tree_partition/tree_partition";
import {TreeSunburst} from "./tree_sunburst/tree_sunburst";
class OLM {
public static CORE = Core;
public static TreePack = TreePack;
public static TreeSunburst = TreeSunburst;
public static TreePartition = TreePartition;
public static TreeIndented = TreeIndented;
}
(<any>document)._OLM = OLM;
This diff is collapsed.
import {FrameworkTree, FrameworkTreeNode} from "../core/framework";
import {Utils} from "../core/utils";
import * as d3 from 'd3';
interface TreePackHierarchy {
name: string,
color: string,
hash: boolean,
children?: Array<TreePackHierarchy>,
src: FrameworkTreeNode,
value?: number
}
interface TreePackD3Data extends d3.HierarchyNode<TreePackHierarchy> {
r: number,
x: number,
y: number
}
interface TreePackConfig {
useLegend?: boolean,
useHash?: boolean,
hashTreshold?: number,
colors?: Array<TreePackColor>,
noValueColor?: string,
formatMastery?: string,
formatCover?: string,
formatTrust?: string,
fontColor?: string,
backgroundColor?: string,
}
interface TreePackColor {
to?: number,
color: string
}
interface TreePackI18NLanguages {
fr: TreePackI18N,
[key: string]: TreePackI18N
}
interface TreePackI18N {
information: {
mastery: {
name: string,
description: string
},
trust: {
name: string,
description: string
},
cover: {
name: string,
description: string
}
},
legend: {
noMastery: string,
masteryBetween: string,
trustTreshold: string
}
}
export class TreePack {
public onMouseOver: (node: FrameworkTreeNode) => void = () => {
};
public onMouseOut: (node: FrameworkTreeNode) => void = () => {
};
// Color settings
private colors: Array<TreePackColor> = [{to: .25, color: "#cf000f"}, {to: .5, color: "#f57f17"}, {
to: .75,
color: "#ffee58"
}, {color: "#4caf50"}];
private noValueColor: string = '#808080';
private fontColor: string = 'rgba(255, 255, 255, .85)';
private backgroundColor: string = '#343a40';
// Hash settings
private useHash: boolean = true;
private hashTreshold: number = 0.1;
// Legend settings
private useLegend: boolean = true;
// Infos displayed
private formatMastery: string = 'percentage'; // '2decimal', 'percentage'
private formatCover: string = 'percentage'; //
private formatTrust: string = 'percentage'; //
// Data
private framework: FrameworkTree = null;
private hierarchy: TreePackHierarchy = null;
// HTMLElements
private elem: HTMLElement = null;
private svg: SVGElement = null;
private svgId: string = null;
private infos: HTMLElement = null;
// D3 consts
private viewBoxWidth: number = 700;
private viewBoxHeight: number = 700;
// I18N
private language: string = 'fr';
readonly i18n: TreePackI18NLanguages = {
'fr': {
'information': {
'mastery': {
'name': 'Taux de maîtrise',
'description': 'Estime ton niveau de maîtrise pour chaque notion, calculé en fonction des travaux effectués en lien avec cette notion.'
},
'trust': {
'name': 'Taux de confiance',
'description': 'Indique la confiance dans le calcul du taux de maîtrise. Cet indice dépend du nombre et de la nature des travaux effectués.'
},
'cover': {
'name': 'Taux de couverture',
'description': 'Indique le pourcentage des sous-notions travaillées.'
}
},
'legend': {
'noMastery': 'Maîtrise non évaluée',
'masteryBetween': 'taux de maîtrise ∈ ',
'trustTreshold': 'taux de confiance < '
}
}
};
constructor(elem: HTMLElement, framework: FrameworkTree, config: TreePackConfig = {}) {
this.elem = elem;
this.framework = FrameworkTree.copy(framework);
this.elem.style.position = 'relative';
this.viewBoxWidth = elem.offsetWidth;
this.viewBoxHeight = elem.offsetHeight;
if (config.useHash !== undefined) this.useHash = config.useHash;
if (config.hashTreshold !== undefined) this.hashTreshold = config.hashTreshold;
if (config.useLegend !== undefined) this.useLegend = config.useLegend;
if (config.colors !== undefined) this.colors = config.colors;
if (config.noValueColor !== undefined) this.noValueColor = config.noValueColor;
if (config.formatMastery !== undefined) this.formatMastery = config.formatMastery;
if (config.formatCover !== undefined) this.formatCover = config.formatCover;
if (config.formatTrust !== undefined) this.formatTrust = config.formatTrust;
if (config.fontColor !== undefined) this.fontColor = config.fontColor;
if (config.backgroundColor !== undefined) this.backgroundColor = config.backgroundColor;
this.hierarchy = this._buildHierarchy();
console.log(this.hierarchy);
let redraw = () => {
this.elem.innerHTML = "";
this.viewBoxWidth = elem.offsetWidth;
this.viewBoxHeight = elem.offsetHeight;
this.draw();
};
redraw.bind(this);
window.addEventListener("resize", redraw);
}
public draw(svgId: string = Utils.makeId()) {
// Creates the svg element. The viewBox dmakes the svg "responsive".
this.svgId = svgId;
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.svg.id = this.svgId;
// Set the boundaries of the element. Ignores the legend offset if we do not display it.
let width: string = this.viewBoxWidth.toString();
let height: string = this.viewBoxHeight.toString();
this.svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
this.svg.setAttribute('preserveAspectRatio', "xMinYMin meet");
this.svg.setAttribute('width', '100%');
this.svg.setAttribute('height', '100%');
this.svg.style.cssText += 'width: 100% !important; height: 100% !important;';
let g: SVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.svg.appendChild(g);
this.elem.appendChild(this.svg);
this._drawInfos();
if (this.useHash) this._drawHash();
this._drawPacks();
if (this.useLegend) this._drawLegend();
}
private _drawInfos(): void {
this.infos = document.createElement('div');
this.infos.innerHTML = `<svg width="20px" height="20px" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
</svg>`;
this.infos.setAttribute('style', 'position: absolute; top: 0; left : 0; cursor: help; color:' + this.fontColor);
let infoPanel = document.createElement('div');
infoPanel.innerHTML = `<h4> Informations </h4>
<hr style="background-color:${this.fontColor} "/>
<p><b>${this.i18n[this.language].information.mastery.name}</b>:<br/>${this.i18n[this.language].information.mastery.description}</p>
<p><b>${this.i18n[this.language].information.trust.name}</b>:<br/>${this.i18n[this.language].information.trust.description}</p>
<p><b>${this.i18n[this.language].information.cover.name}</b>:<br/>${this.i18n[this.language].information.cover.description}</p>`;
let infoPanelStyle: string = `border-radius: 15px;
opacity: .95;
position: absolute;
padding: 30px;
top: 0;
left: 0;
color: ${this.fontColor};
background-color: ${this.backgroundColor};
width: ${this.viewBoxWidth.toString()}px;
height: ${this.viewBoxHeight.toString()}px;
display: none`;
infoPanel.setAttribute('style', infoPanelStyle);
this.elem.appendChild(infoPanel);
this.elem.appendChild(this.infos);
this.infos.addEventListener('mouseenter', () => {
infoPanel.style.display = 'block';
});
this.infos.addEventListener('mouseleave', () => {
infoPanel.style.display = 'none';
});
}
private _drawHash(): void {
let s = 4;
d3.select(this.svg)
.append("defs")
.append('pattern')
.attr('id', 'diagonalHatch')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', s)
.attr('height', s)
.append('circle')
.attr('cx', s / 2)
.attr('cy', s / 2)
.attr('r', 1)
.attr('stroke-width', 0)
.attr('fill', '#404040');
// Diagonal Hash
// .append('path')
// .attr('d', `M 0,${s} l ${s},${-s} M ${-s / 4},${s / 4} l ${s / 2},${-s / 2} M ${3 / 4 * s},${5 / 4 * s} l ${s / 2},${-s / 2}`)
// .attr('stroke-width', 2);
}
private _drawPacks(): void {
let packLayout = d3.pack();
let offset: number = (this.useLegend) ? ((this.viewBoxWidth - this.viewBoxHeight) / 2) - 10 : 0;
packLayout.size([this.viewBoxWidth, this.viewBoxHeight]);
packLayout.padding(10);
let root = d3.hierarchy(this.hierarchy);
root.sum(d => d.value);
console.log(root);
packLayout(root);
// Note: Use the first 'g' child of this.svg to encapsulate the node into a big circle.
let packNodes = d3.select(this.svg)
.selectAll('g')
.data(root.descendants())
.enter()
.append('g').attr('class', 'node')
.style('cursor', 'pointer')
.attr('transform', (d: TreePackD3Data) => 'translate(' + [d.x - offset, d.y] + ')')
.on('mouseover', this._handleMouseOver.bind(this))
.on('mouseout', this._handleMouseOut.bind(this));
packNodes
.append('circle')
.classed('the-node', true)
.attr('id', d => 'olm-pack-' + d.data.src.id)
.attr('class', 'olm-tree-pack-event-targetable the-node')
.attr('r', (d: TreePackD3Data) => d.r)
.style('fill', d => d.data.color)
.style('stroke', '#2f2f2f');
console.log(packNodes);
packNodes
.filter(d => d.data.hash)
.append("circle")
.classed('the-node', true)
.attr('r', (d: TreePackD3Data) => d.r)
.attr('fill', 'url(#diagonalHatch)');
}
private _drawLegend(): void {
let packNodes = d3.select(this.svg)
let size: number = 16;
let x: number = this.viewBoxHeight + 10;
let y: number = this.viewBoxHeight - 5 * (size + 10);
let _generic_draw = (color: string, text: string) => {
packNodes.append("rect")
.attr("x", x)
.attr("y", y) // 100 is where the first dot appears. 25 is the distance between dots
.attr("width", size)
.attr("height", size)
.style("fill", color);
packNodes.append("text")
.attr("x", x + size + 10)
.attr("y", y + size) // 100 is where the first dot appears. 25 is the distance between dots
.style("fill", this.fontColor)
.text(text)
.attr("text-anchor", "left")
.style("alignment-baseline", "baseline")
}
// No mastery score
_generic_draw(this.noValueColor, this.i18n[this.language].legend.noMastery);
y += size + 10;
for (let i = 0; i < this.colors.length; i++) {
let text: string = this.i18n[this.language].legend.masteryBetween;
if (i === 0) text += `[${this._formatValue('mastery', 0)}`;
else text += `]${this._formatValue('mastery', this.colors[i - 1].to)}`;
if (i === this.colors.length - 1) text += `,${this._formatValue('mastery', 1)}]`;
else text += `,${this._formatValue('mastery', this.colors[i].to)}]`;
_generic_draw(this.colors[i].color, text);
y += size + 10;
}
// Trust score legend
if (this.useHash) {
_generic_draw('rgba(255, 255, 2555, .8)', this.i18n[this.language].legend.trustTreshold + this._formatValue('trust', this.hashTreshold));
packNodes.append("rect")
.attr("x", x)
.attr("y", y) // 100 is where the first dot appears. 25 is the distance between dots
.attr("width", size)
.attr("height", size)
.style("fill", 'url(#diagonalHatch)');
}
}
private _drawDetails(d: TreePackD3Data): void {
let y = 16;
let packNodes = d3.select(this.svg);
packNodes.append("text")
.attr("x", this.viewBoxHeight + 10)
.attr("y", y)
.style("fill", this.fontColor)
.text(d.data.src.data.name)
.attr('class', 'olm-treepack-label')
.attr("text-anchor", "left")
.style("alignment-baseline", "baseline")
.style("font-weight", 'bold');
y += 16 + 10;
packNodes.append("text")
.attr("x", this.viewBoxHeight + 10)
.attr("y", y)
.style("fill", this.fontColor)
.text(`Taux de maîtrise : ${this._formatValue('mastery', d.data.src.data.mastery)}`)
.attr('class', 'olm-treepack-label')
.attr("text-anchor", "left")
.style("alignment-baseline", "baseline");
y += 16 + 10;
packNodes.append("text")
.attr("x", this.viewBoxHeight + 10)
.attr("y", y)
.style("fill", this.fontColor)
.text("Taux de confiance : " + this._formatValue('trust', d.data.src.data.trust))
.attr('class', 'olm-treepack-label')
.attr("text-anchor", "left")
.style("alignment-baseline", "baseline");
y += 16 + 10;
packNodes.append("text")
.attr("x", this.viewBoxHeight + 10)
.attr("y", y)
.style("fill", this.fontColor)
.text("Taux de couverture : " + this._formatValue('cover', d.data.src.data.cover))
.attr('class', 'olm-treepack-label')
.attr("text-anchor", "left")
.style("alignment-baseline", "baseline");
}
public getSVGId(): string {
return this.svgId;
}
/**
* Builds hierarchy
* @returns hierarchy
*/
private _buildHierarchy(): TreePackHierarchy {
function recurse(currentNode: FrameworkTreeNode) {
let data_node: TreePackHierarchy = null;
// Sets the colors. If we got a leaf, put grey if trust > 0
let color = this.noValueColor;
if (currentNode.data.mastery === undefined) color = this.noValueColor;
else if ((currentNode.children.length !== 0 && currentNode.data.cover > 0) || currentNode.data.trust > 0) {
let i = 0;
color = this.colors[i].color;
while (i < this.colors.length - 1 && currentNode.data.mastery > this.colors[i].to) {
i++;
color = this.colors[i].color;
}
}
// Creates the tree pack hirarchy objects.
// With children if not a leaf, with value = 1 otherwise.
if (currentNode.children.length > 0) {
data_node = {
'name': currentNode.data.name,
'children': [],
'color': color,
'hash': (this.useHash && currentNode.data.trust < this.hashTreshold),
'src': currentNode
}
} else {
data_node = {
'name': currentNode.data.name,
'color': color,
'value': 1,
'hash': (this.useHash && currentNode.data.trust < this.hashTreshold),
'src': currentNode
}
}
// Creates the children of the current node if they exist.
for (var i = 0, length = currentNode.children.length; i < length; i++) {
let nextNode = this.framework.getNode(currentNode.children[i]);
data_node.children.push(recurse.bind(this, nextNode)());
}
return data_node;
}
return recurse.bind(this, this.framework.getRoot())();
}
private _handleMouseOver(e: any, d: TreePackD3Data): void {
let id = 'olm-pack-' + d.data.src.id;
d3.select(document.getElementById(id))
.transition().duration(200)
.style('fill', "rgba(211,211,211,0.8)");
this._drawDetails(d);
this.onMouseOver(d.data.src);
}
private _handleMouseOut(e: any, d: TreePackD3Data): void {
let id = 'olm-pack-' + d.data.src.id;
d3.select(document.getElementById(id))
.transition().duration(200)
.style('fill', d.data.color)
d3.selectAll(".olm-treepack-label").remove();
this.onMouseOut(d.data.src);
}
// Utilities ---------------------------------------------------------------
private _formatValue(type: string, value: number): string {
if (isNaN(value) || value === undefined || value === null) return '-';
switch (type) {
case "mastery":
switch (this.formatMastery) {
case '1decimal':
return (Math.round(value * 10) / 10).toString();
case '2decimal':
return (Math.round(value * 100) / 100).toString();
case 'percentage':
return Math.round(value * 100).toString() + '%';
}
break;
case "cover":
switch (this.formatCover) {
case 'percentage':
return Math.round(value * 100).toString() + '%';
}
break;
case "trust":
switch (this.formatTrust) {
case 'percentage':
return Math.round(value * 100).toString() + '%';
}
break;
}
return 'a';
}
}
import {FrameworkTree, FrameworkTreeNode} from "../core/framework";
import {Utils} from "../core/utils";
import * as d3 from 'd3';
interface TreePartitionHierarchy {
name: string,
color: string,
hash: boolean,
children?: Array<TreePartitionHierarchy>,
src: FrameworkTreeNode,
value?: number,
}
interface TreePartitionD3Data extends d3.HierarchyNode<TreePartitionHierarchy> {
x0: number,
y0: number,
x1: number,
y1: number,
data: TreePartitionHierarchy
}
interface TreePartitionConfig {
useLegend?: boolean,
useHash?: boolean,
hashTreshold?: number,
colors?: Array<TreePartitionColor>,
noValueColor?: string,
formatMastery?: string,
formatCover?: string,
formatTrust?: string,
fontColor?: string,
backgroundColor?: string
}
interface TreePartitionColor {
to?: number,
color: string
}
interface TreePartitionI18NLanguages {
fr: TreePartitionI18N,
[key: string]: TreePartitionI18N
}
interface TreePartitionI18N {
information: {
mastery: {
name: string,
description: string
},
trust: {
name: string,
description: string
},
cover: {
name: string,
description: string
}
},
legend: {
noMastery: string,
masteryBetween: string,
trustTreshold: string
}
}
export class TreePartition {
public onMouseOver: (node: FrameworkTreeNode) => void = () => {
};
public onMouseOut: (node: FrameworkTreeNode) => void = () => {
};
// Color settings
private colors: Array<TreePartitionColor> = [{to: .25, color: "#cf000f"}, {to: .5, color: "#f57f17"}, {
to: .75,
color: "#ffee58"
}, {color: "#4caf50"}];
private noValueColor: string = '#808080';
private fontColor: string = 'rgba(255, 255, 255, .85)';
private backgroundColor: string = '#343a40';
// Hash settings
private useHash: boolean = true;
private hashTreshold: number = 0;
// Legend settings
private useLegend: boolean = true;
// Infos displayed
private formatMastery: string = 'percentage'; // '2decimal', 'percentage'
private formatCover: string = 'percentage'; //
private formatTrust: string = 'percentage'; //
// Data
private framework: FrameworkTree = null;
private hierarchy: TreePartitionHierarchy = null;
// HTMLElements
private elem: HTMLElement = null;
private svg: SVGElement = null;
private svgG: SVGElement = null;
private svgId: string = null;
private infos: HTMLElement = null;
// D3 consts
private viewBoxWidth: number = 800;
private viewBoxHeight: number = 400;
// I18N
private language: string = 'fr';
readonly i18n: TreePartitionI18NLanguages = {
'fr': {
'information': {
'mastery': {
'name': 'Taux de maîtrise',
'description': 'Estime ton niveau de maîtrise pour chaque notion, calculé en fonction des travaux effectués en lien avec cette notion.'
},
'trust': {
'name': 'Taux de confiance',
'description': 'Indique la confiance dans le calcul du taux de maîtrise. Cet indice dépend du nombre et de la nature des travaux effectués.'
},
'cover': {
'name': 'Taux de couverture',
'description': 'Indique le pourcentage des sous-notions travaillées.'
}
},
'legend': {
'noMastery': 'Maîtrise non évaluée',
'masteryBetween': 'maîtrise ∈ ',
'trustTreshold': 'confiance < '
}
}
};
constructor(elem: HTMLElement, framework: FrameworkTree, config: TreePartitionConfig = {}) {
this.elem = elem;
this.framework = FrameworkTree.copy(framework);
this.elem.style.position = 'relative';
this.viewBoxWidth = elem.offsetWidth;
this.viewBoxHeight = elem.offsetHeight;
if (config.useHash !== undefined) this.useHash = config.useHash;
if (config.hashTreshold !== undefined) this.hashTreshold = config.hashTreshold;
if (config.useLegend !== undefined) this.useLegend = config.useLegend;
if (config.colors !== undefined) this.colors = config.colors;
if (config.noValueColor !== undefined) this.noValueColor = config.noValueColor;
if (config.formatMastery !== undefined) this.formatMastery = config.formatMastery;
if (config.formatCover !== undefined) this.formatCover = config.formatCover;
if (config.formatTrust !== undefined) this.formatTrust = config.formatTrust;
if (config.fontColor !== undefined) this.fontColor = config.fontColor;
if (config.backgroundColor !== undefined) this.backgroundColor = config.backgroundColor;
this.hierarchy = this._buildHierarchy();
let redraw = () => {
this.elem.innerHTML = "";
this.viewBoxWidth = elem.offsetWidth;
this.viewBoxHeight = elem.offsetHeight;
this.draw();
};
redraw.bind(this);
window.addEventListener("resize", redraw);
}
public draw(svgId: string = Utils.makeId()) {
// Creates the svg element. The viewBox dmakes the svg "responsive".
this.svgId = svgId;
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.svg.id = this.svgId;
// Set the boundaries of the element. Ignores the legend offset if we do not display it.
let width: string = this.viewBoxWidth.toString();
let height: string = this.viewBoxHeight.toString();
this.svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
this.svg.setAttribute('width', '100%');
this.svg.setAttribute('height', '100%');
this.svg.style.cssText += 'width: 100% !important; height: 100% !important;';
this.svgG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.svg.appendChild(this.svgG);
this.elem.appendChild(this.svg);
this._drawInfos();
if (this.useHash) this._drawHash();
this._drawPacks();
if (this.useLegend) this._drawLegend();
}
private _drawInfos(): void {
this.infos = document.createElement('div');
this.infos.innerHTML = `<svg width="20px" height="20px" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
</svg>`;
this.infos.setAttribute('style', 'position: absolute; top: 0; left : 0; cursor: help; color:' + this.fontColor);
let infoPanel = document.createElement('div');
infoPanel.innerHTML = `<h4> Informations </h4>
<hr style="background-color:${this.fontColor} "/>
<p><b>${this.i18n[this.language].information.mastery.name}</b>:<br/>${this.i18n[this.language].information.mastery.description}</p>
<p><b>${this.i18n[this.language].information.trust.name}</b>:<br/>${this.i18n[this.language].information.trust.description}</p>
<p><b>${this.i18n[this.language].information.cover.name}</b>:<br/>${this.i18n[this.language].information.cover.description}</p>`;
let infoPanelStyle: string = `border-radius: 15px;
opacity: .95;
position: absolute;
padding: 30px;
top: 0;
left: 0;
color: ${this.fontColor};
background-color: ${this.backgroundColor};
width: ${this.viewBoxWidth.toString()}px;
height: ${this.viewBoxHeight.toString()}px;
display: none`;
infoPanel.setAttribute('style', infoPanelStyle);
this.elem.appendChild(infoPanel);
this.elem.appendChild(this.infos);
this.infos.addEventListener('mouseenter', () => {
infoPanel.style.display = 'block';
});
this.infos.addEventListener('mouseleave', () => {
infoPanel.style.display = 'none';
});
}
private _drawHash(): void {
let s = 4;
d3.select(this.svg)
.append("defs")
.append('pattern')
.attr('id', 'diagonalHatch')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', s)
.attr('height', s)
.append('circle')
.attr('cx', s / 2)
.attr('cy', s / 2)
.attr('r', 1)
.attr('stroke-width', 0)
.attr('fill', '#404040');
// Diagonal Hash
// .append('path')
// .attr('d', `M 0,${s} l ${s},${-s} M ${-s / 4},${s / 4} l ${s / 2},${-s / 2} M ${3 / 4 * s},${5 / 4 * s} l ${s / 2},${-s / 2}`)
// .attr('stroke-width', 2);
}
private _drawPacks(): void {
var partitionLayout = d3.partition();
let offsetHeight: number = (5 * 26) + 10; // THe legend height + 10 padding
let height: number = this.viewBoxHeight - offsetHeight;
let width: number = this.viewBoxWidth;
partitionLayout.size([width, height]);
// partitionLayout.padding(2);
let root = d3.hierarchy(this.hierarchy);
root.sum(d => d.value);
partitionLayout(root);
var partitionNodes = d3.select(this.svgG)
.selectAll("g")
.data(root.descendants())
.enter()
.append('g').attr('class', 'node')
.attr('transform', (d: TreePartitionD3Data) => 'translate(' + [d.x0, d.y0 + offsetHeight] + ')')
.on('mouseover', this._handleMouseOver.bind(this))
.on('mouseout', this._handleMouseOut.bind(this));
partitionNodes
.append('rect')
.classed('the-node', true)
.attr('id', d => 'olm-partition-' + d.data.src.id)
.attr('width', (d: TreePartitionD3Data) => d.x1 - d.x0)
.attr('height', (d: TreePartitionD3Data) => d.y1 - d.y0)
.style('fill', (d: TreePartitionD3Data) => d.data.color)
.style('cursor', 'pointer')
.style('stroke', '#2f2f2f')
partitionNodes
.filter(d => d.data.hash)
.append('rect')
.classed('the-node', true)
.attr('width', (d: TreePartitionD3Data) => d.x1 - d.x0)
.attr('height', (d: TreePartitionD3Data) => d.y1 - d.y0)
.attr('fill', 'url(#diagonalHatch)')
.style('cursor', 'pointer')
.style('stroke', '#2f2f2f')
}
private _drawLegend(): void {
let packNodes = d3.select(this.svg)
let size: number = 16;
let x: number = 10 + 20; // 20 is help icon button width;
let y: number = 0;
let _generic_draw = (color: string, text: string) => {
packNodes.append("rect")
.attr("x", x)
.attr("y", y) // 100 is where the first dot appears. 25 is the distance between dots
.attr("width", size)
.attr("height", size)
.style("fill", color);
packNodes.append("text")
.attr("x", x + size + 10)
.attr("y", y + size) // 100 is where the first dot appears. 25 is the distance between dots
.style("fill", this.fontColor)
.text(text)
.attr("text-anchor", "left")
.style("alignment-baseline", "baseline")
}
// No mastery score
_generic_draw(this.noValueColor, this.i18n[this.language].legend.noMastery);
y += size + 10;
for (let i = 0; i < this.colors.length; i++) {
let text: string = this.i18n[this.language].legend.masteryBetween;
if (i === 0) text += `[${this._formatValue('mastery', 0)}`;
else text += `]${this._formatValue('mastery', this.colors[i - 1].to)}`;
if (i === this.colors.length - 1) text += `,${this._formatValue('mastery', 1)}]`;
else text += `,${this._formatValue('mastery', this.colors[i].to)}]`;
_generic_draw(this.colors[i].color, text);
y += size + 10;
}
// Trust score legend
if (this.useHash) {
_generic_draw('rgba(255, 255, 2555, .8)', this.i18n[this.language].legend.trustTreshold + this._formatValue('trust', this.hashTreshold));
packNodes.append("rect")
.attr("x", x)
.attr("y", y) // 100 is where the first dot appears. 25 is the distance between dots
.attr("width", size)
.attr("height", size)
.style("fill", 'url(#diagonalHatch)');
}
}
private _drawDetails(d: TreePartitionD3Data): void {
let y = 16;
let x = 220;
let packNodes = d3.select(this.svg);
packNodes.append("text")
.attr("x", x)
.attr("y", y)
.style("fill", this.fontColor)
.text(d.data.src.data.name)
.attr('class', 'olm-treepack-label')
.attr("text-anchor", "left")
.style("alignment-baseline", "baseline")
.style("font-weight", 'bold');
y += 16 + 10;
packNodes.append("text")
.attr("x", x)
.attr("y", y)
.style("fill", this.fontColor)
.text(`Taux de maîtrise : ${this._formatValue('mastery', d.data.src.data.mastery)}`)
.attr('class', 'olm-treepack-label')
.attr("text-anchor", "left")
.style("alignment-baseline", "baseline");
y += 16 + 10;
packNodes.append("text")
.attr("x", x)
.attr("y", y)
.style("fill", this.fontColor)
.text("Taux de confiance : " + this._formatValue('trust', d.data.src.data.trust))
.attr('class', 'olm-treepack-label')
.attr("text-anchor", "left")
.style("alignment-baseline", "baseline");
y += 16 + 10;
packNodes.append("text")
.attr("x", x)
.attr("y", y)
.style("fill", this.fontColor)
.text("Taux de couverture : " + this._formatValue('cover', d.data.src.data.cover))
.attr('class', 'olm-treepack-label')
.attr("text-anchor", "left")
.style("alignment-baseline", "baseline");
}
public getSVGId(): string {
return this.svgId;
}
/**
* Builds hierarchy
* @returns hierarchy
*/
private _buildHierarchy(): TreePartitionHierarchy {
function recurse(currentNode: FrameworkTreeNode) {
let data_node: TreePartitionHierarchy = null;
// Sets the colors. If we got a leaf, put grey if trust > 0
let color = this.noValueColor;
if (currentNode.data.mastery === undefined) color = this.noValueColor;
else if ((currentNode.children.length !== 0 && currentNode.data.cover > 0) || currentNode.data.trust > 0) {
let i = 0;
color = this.colors[i].color;
while (i < this.colors.length - 1 && currentNode.data.mastery > this.colors[i].to) {
i++;
color = this.colors[i].color;
}
}
// Creates the tree pack hirarchy objects.
// With children if not a leaf, with value = 1 otherwise.
if (currentNode.children.length > 0) {
data_node = {
'name': currentNode.data.name,
'children': [],
'color': color,
'hash': (this.useHash && currentNode.data.trust < this.hashTreshold),
'src': currentNode
}
} else {
data_node = {
'name': currentNode.data.name,
'color': color,
'value': 1,
'hash': (this.useHash && currentNode.data.trust < this.hashTreshold),
'src': currentNode
}
}
// Creates the children of the current node if they exist.
for (var i = 0, length = currentNode.children.length; i < length; i++) {
let nextNode = this.framework.getNode(currentNode.children[i]);
data_node.children.push(recurse.bind(this, nextNode)());
}
return data_node;
}
return recurse.bind(this, this.framework.getRoot())();
}
private _handleMouseOver(e: any, d: TreePartitionD3Data): void {
let id = 'olm-partition-' + d.data.src.id;
d3.select(document.getElementById(id))
.transition().duration(200)
.style('fill', "rgba(211,211,211,0.8)")
this._drawDetails(d);
this.onMouseOver(d.data.src);
}
private _handleMouseOut(e: any, d: TreePartitionD3Data): void {
let id = 'olm-partition-' + d.data.src.id;
d3.select(document.getElementById(id))
.transition().duration(200)
.style('fill', d.data.color);
d3.selectAll(".olm-treepack-label").remove();
this.onMouseOut(d.data.src);
}
// Utilities ---------------------------------------------------------------
private _formatValue(type: string, value: number): string {
if (isNaN(value) || value === undefined || value === null) return '-';
switch (type) {
case "mastery":
switch (this.formatMastery) {
case '1decimal':
return (Math.round(value * 10) / 10).toString();
case '2decimal':
return (Math.round(value * 100) / 100).toString();
case 'percentage':
return Math.round(value * 100).toString() + '%';
}
break;
case "cover":
switch (this.formatCover) {
case 'percentage':
return Math.round(value * 100).toString() + '%';
}
break;
case "trust":
switch (this.formatTrust) {
case 'percentage':
return Math.round(value * 100).toString() + '%';
}
break;
}
return 'a';
}
}
This diff is collapsed.
This diff is collapsed.
import { Core } from "./core/core"
import { TreeIndented } from "./tree_indented/tree_indented";
import { TreePack } from "./tree_pack/tree_pack";
import { TreePartition } from "./tree_partition/tree_partition";
import { TreeSunburst } from "./tree_sunburst/tree_sunburst";
class OLM {
public static CORE = Core;
public static TreePack = TreePack;
public static TreeSunburst = TreeSunburst;
public static TreePartition = TreePartition;
public static TreeIndented = TreeIndented;
}
(<any>document)._OLM = OLM;
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="x-ua-compatible" content="ie=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!-- <link rel="stylesheet" href="../../css/olm.css"> -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<script type="text/javascript" src="../../dist/olm.bundle.js"></script>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/overlayscrollbars/1.12.0/css/OverlayScrollbars.min.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/overlayscrollbars/1.12.0/js/OverlayScrollbars.min.js"></script>
<title>OLM Tree Pack Demo</title>
<style>
html, body {
margin: 0px;
background: #32333b;
min-height: 100%;
font-family: sans-serif;
font-size: 0.9rem;
}
com {
font-style: italic;
color: #96e6ff;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12 text-light mt-4 p-0">
<h3>OLM TreePack</h3>
</div>
</div>
<div class="row">
<div class="col-8 text-light mt-4 p-0 pr-4">
<h5>How to</h5>
<hr class="bg-light m-0"/>
<pre>
<code class="text-light">
<com>// Creates a sample framework randomly scored. <b>This should be replaced with some framework retrieving function.</b> </com>
let framework = OLM.CORE.Utils.getScoredFrameworkSample();
<com>// Creates a tree based on the framework.</com>
let <span style="color: #b700b7">fw_tree</span> = new OLM.CORE.FrameworkTree();
<span style="color: #b700b7">fw_tree</span>.buildFromFramework(framework);
<com>// Creates the TreePack object. The <span
class="text-primary">config</span> is editable on the right <b>=></b> </com>
let treePack = new OLM.TreePack(document.getElementById('test'), <span style="color: #b700b7">fw_tree</span>, <span
class="text-primary">config</span>);
treePack.onMouseOver = (node) => {
<com>// Your mouseOver behavior here. In the exemple below, we just populate the right panel with the node infos.</com>
}
<com>// We chose an id for the svg element. Default behavior automatically creates a unique id.</com>
treePack.draw(svgId = 'test-pack');
</code>
</pre>
</div>
<div class="col-4 text-light mt-4 p-0">
<div class="row">
<div class="col-12 overflow-auto mb-3" style="height:300px" id="config-container">
<pre class="text-light bg-secondary p-3 mb-0 rounded" id="config" contenteditable="true"
spellcheck="false"></pre>
</div>
</div>
<div class="row">
<div class="col-6">
<button type="button" onclick="updateChart()" class="btn btn-outline-primary w-100">Update</button>
</div>
<div class="col-6">
<button type="button" onclick="resetChart()" class="btn btn-outline-warning w-100">Reset</button>
</div>
</div>
</div>
</div>
<div class="row" style="height:450px;">
<div class="col-12 text-light p-0">
<h5>Example</h5>
</div>
<div class="col h-100 p-2 border rounded text-light bg-dark mr-4">
<div id="test" class="h-100"></div>
</div>
</div>
</div>
<div id="alert" class="alert alert-danger d-none fixed-bottom ml-4 mr-4" role="alert">
<h4>Config error <span class="text-alert font-weight-bold float-right" style="cursor:pointer"
onclick="dismissAlert()">&times;</span></h4>
<hr/>
<p id="alert-content"></p>
</div>
</body>
<script>
var OLM = null;
var fw_tree = null;
var default_config = {
"fontColor": 'rgba(255, 255, 255, .85)',
"backgroundColor": '#343a40',
"formatMastery": 'percentage',
"formatTrust": 'percentage',
"formatCover": 'percentage',
"useHash": true,
"hashTreshold": 0.1,
"useLegend": true,
"colors": [{
"to": 0.4,
"color": "#cf000f"
}, {
"to": 0.8,
"color": "#f7ca18"
}, {
"color": "#00b16a"
}],
"noValueColor": "#808080"
};
var config = {...default_config};
(function () {
test();
})();
document.addEventListener("DOMContentLoaded", function () {
//The first argument are the elements to which the plugin shall be initialized
//The second argument has to be at least a empty object or a object with your desired options
OverlayScrollbars([document.getElementById('config-container')], {className: "os-theme-light"});
});
function dismissAlert() {
document.getElementById('alert').classList.add('d-none');
}
function updateChart() {
dismissAlert();
config = document.getElementById('config').textContent;
document.getElementById('test').innerHTML = "";
try {
config = JSON.parse(config);
draw();
} catch (err) {
document.getElementById('alert-content').textContent = err;
document.getElementById('alert').classList.remove('d-none');
}
}
function resetChart() {
dismissAlert();
config = {...default_config};
document.getElementById('config').textContent = JSON.stringify(config, null, 4);
document.getElementById('test').innerHTML = "";
draw();
}
function test() {
OLM = document._OLM;
let framework = OLM.CORE.Utils.getScoredFrameworkSample();
fw_tree = new OLM.CORE.FrameworkTree();
fw_tree.buildFromFramework(framework);
document.getElementById('config').textContent = JSON.stringify(config, null, 4);
draw();
}
function draw() {
let treePack = new OLM.TreePack(document.getElementById('test'), fw_tree, config);
treePack.onMouseOver = (node) => {
// console.log(node);
}
treePack.draw(svgId = 'test-pack');
}
</script>
</html>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment