diff --git a/.env.exemple b/.env.exemple new file mode 100644 index 0000000..d01e681 --- /dev/null +++ b/.env.exemple @@ -0,0 +1,6 @@ +NODE_ENV=development +PORT=3005 +mysqlHost=localhost +mysqlUser=user +mysqlPassword=password +mysqlDatabase=databasename diff --git a/README.md b/README.md index 696f84f..d421eca 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,22 @@ # Capuchin Follow up to the famous Rezbuild-baboon project with an all new UI. + +## install + +1. Clone the project from : https://github.com/dhmmasson/rezbuild-emperor-tamarin.git +1. Create the database and populate the database + 1. Create an empty database on mysql + 2. Import db/structure_*.sql ( you may have to reorder the elements ) + 3. import dataset db/*dataset*.sql +1. rename .env.exemple .env and modify .env file with database information (host, name, user, password) +1. install libraries : npm install +1. test by launching the app : npm start + +## development + +1. install nodemon : npm install nodemon +1. launch the app : + * nodemon ./bin/www.mjs + * Or a better version : + * nodemon -i public/javascripts -e js,pug,mjs,cjs bin/www.mjs diff --git a/app.mjs b/app.mjs index 4e6f361..b80b8da 100644 --- a/app.mjs +++ b/app.mjs @@ -35,6 +35,7 @@ app.use( "/javascripts", express.static( path.join( process.env.PWD, "node_modul app.use( "/javascripts", express.static( path.join( process.env.PWD, "node_modules/materialize-css/dist/js" ) ) ) ; app.use( "/javascripts", express.static( path.join( process.env.PWD, "node_modules/@svgdotjs/svg.draggable.js/dist" ) ) ) ; app.use( "/javascripts", express.static( path.join( process.env.PWD, "node_modules/@svgdotjs/svg.js/dist" ) ) ) ; +app.use( "/javascripts", express.static( path.join( process.env.PWD, "node_modules/papaparse" ) ) ) ; app.use( "/fonts", express.static( path.join( process.env.PWD, "node_modules/materialize-css/dist/fonts" ) ) ) ; diff --git a/package-lock.json b/package-lock.json index 71f4018..efb202d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3006,9 +3006,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash.camelcase": { "version": "4.3.0", @@ -3860,6 +3860,11 @@ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, + "papaparse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.2.0.tgz", + "integrity": "sha512-ylq1wgUSnagU+MKQtNeVqrPhZuMYBvOSL00DHycFTCxownF95gpLAk1HiHdUW77N8yxRq1qHXLdlIPyBSG9NSA==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/package.json b/package.json index dbb3f0b..606fc0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rezbuild-emperor-tamarin", - "version": "1.4.3", + "version": "1.5.0", "private": true, "scripts": { "start": "node ./bin/www.mjs" @@ -18,6 +18,7 @@ "morgan": "~1.9.1", "mysql2": "^2.1.0", "node-sass-middleware": "0.11.0", + "papaparse": "^5.2.0", "pug": "^2.0.4" }, "description": "Rezbuild Emperor Tamarin is a tool to explore a set of product by ordering a set of criteria", diff --git a/public/javascripts/Downloader.mjs b/public/javascripts/Downloader.mjs new file mode 100644 index 0000000..0436435 --- /dev/null +++ b/public/javascripts/Downloader.mjs @@ -0,0 +1,64 @@ + +const formater = new Intl.DateTimeFormat( "en", + { year : "2-digit" + , month : "2-digit" + , day : "2-digit" + , hour : "2-digit" + , minute : "2-digit" + , second : "2-digit" + } ) ; +const formatedDate = () => { + const date = formater.formatToParts( new Date() ).reduce( + ( acc, e ) => { + acc[ e.type ] = e.value ; + return acc ; + }, {} ) ; + return `${ date.year }${ date.month }${ date.day }_${ date.hour }${ date.minute }${ date.second }` ; +} ; +const paramExportCSV = { +quotes : true // or array of booleans +, quoteChar : "\"" +, escapeChar : "\"" +, delimiter : "," +, header : true +, newline : "\r\n" +, skipEmptyLines : true // or 'greedy', +, columns : null // or array of strings +} ; + +/** + * @class + */ +export class Downloader { + + + /** + * constructor - description + * + * @param {type} htmlElement htlmRoot element of the button, must contain an anchor element () + * @return {type} description + */ + constructor( htmlElement ) { + [ this.anchor ] = $( htmlElement ).find( "a" ) ; + $( htmlElement ).click( event => { + console.log( "click" ) ; + this.saveData() ; + } ) ; + console.log( htmlElement, this.anchor ) ; + } + + // Update the csv to be downloaded + updateCSV( data ) { + if( this.anchor.href ) window.URL.revokeObjectURL( this.anchor.href ) ; + this.csv = Papa.unparse( { fields : [ "name", "description", "score" ] + , data : data.sorted }, paramExportCSV ) ; + const blob = new Blob( [ this.csv ] + , { type: "text/csv" } ) + , url = window.URL.createObjectURL( blob ) ; + this.anchor.href = url ; + } + + saveData() { + this.anchor.download = `data_${ formatedDate() }.csv` ; + } +} diff --git a/public/javascripts/UI/label.mjs b/public/javascripts/UI/label.mjs index 8e4d1f1..728ce72 100644 --- a/public/javascripts/UI/label.mjs +++ b/public/javascripts/UI/label.mjs @@ -27,6 +27,8 @@ class Label { * * @param {Object} offset.y y coordinate for the label * @param {module:Models~Criterion} criterion description */ + + constructor( twoDimensionControlPanel, offset, criterion, callback ) { this.panel = twoDimensionControlPanel ; this.criterion = criterion ; @@ -45,10 +47,10 @@ class Label { this.callback = callback ; this.label = this.labelsGroup - .text( criterion.description ) - .move( offset.x, offset.y ) - .fill( this.color ) - .mousedown( event => { this.onMousedown( event ) ; } ) ; + .text( criterion.description ) + .move( offset.x, offset.y ) + .fill( this.color ) + .mousedown( event => { this.onMousedown( event ) ; } ) ; this.labelBbox = this.label.bbox() ; this.LineOrigin = { x : this.labelBbox.x2 @@ -68,6 +70,8 @@ class Label { } + + // add offset set stageBox( box ) { const { x2, y2 } = box ; @@ -77,13 +81,13 @@ class Label { } /** - * onMousedown - On clicking over the label create an elipse and start dragging it + * onMousedown - On clicking over the label create an ellipse and start dragging it * if the ellipse exist don't create it. * * @param {type} event description * @return {type} description */ - onMousedown( event ) { + onMousedown( event ) { if( this.ellipse === null ) { this.createHandle( event.offsetX, event.offsetY ) ; } diff --git a/public/javascripts/UI/twoDimensionControlPanel.mjs b/public/javascripts/UI/twoDimensionControlPanel.mjs index 9bc2c0d..136c500 100644 --- a/public/javascripts/UI/twoDimensionControlPanel.mjs +++ b/public/javascripts/UI/twoDimensionControlPanel.mjs @@ -31,7 +31,7 @@ class UI { constructor( root, criteria, callback ) { this.dimensions = ( { width : "100%" - , height : "300px" } ) ; + , height : "500px" } ) ; this.restAreaWidth = 100 ; this._initSvg( root, this.dimensions ).then( diff --git a/public/javascripts/capuchin.mjs b/public/javascripts/capuchin.mjs index ab61d00..dbd88e6 100644 --- a/public/javascripts/capuchin.mjs +++ b/public/javascripts/capuchin.mjs @@ -1,31 +1,40 @@ import { Sorter } from "./Sorter.mjs" ; import { UI } from "./UI/twoDimensionControlPanel.mjs" ; import { template } from "./template.mjs" ; +import { Downloader } from "./Downloader.mjs" ; import * as SVGmodule from "./svg.esm.js" ; window.SVG = SVGmodule ; -console.log( window.SVG -) ; const sorter = new Sorter( [], [] ) ; let ui = null ; -$( getCriteria() ) ; + + +$( loadData() ) ; + + +function loadData( ) { + getCriteria() ; + getTechnologies() ; +} function getCriteria( ) { $.get( "/api/criteria", function( data ) { sorter.criteria = data.criteria ; ui = new UI( $( "#controlPanel" )[ 0 ], sorter.criteria.all, () => initSorter() ) ; - + } ) ; +} +function getTechnologies( ) { + $.get( "/api/technologies", function( data ) { + sorter.technologies = data.technologies ; + initSorter() ; } ) ; } - -$.get( "/api/technologies", function( data ) { - sorter.technologies = data.technologies ; - initSorter() ; -} ) ; - +let onlyOnce = true ; function initSorter() { - if( sorter.criteria.all.length > 0 && sorter.technologies.all.length > 0 ) { + if( sorter.criteria.all.length > 0 && sorter.technologies.all.length > 0 && onlyOnce ) { attachEventListener() ; loadState() ; + onlyOnce = false ; + $( "#controlPanel" ).mouseup( () => { setTimeout( updateTable, 100 ) ; } ) ; } } @@ -35,14 +44,21 @@ function loadState( ) { } function attachEventListener () { + console.log( "attachEventListener" ) ; + const downloader = new Downloader( $( "#saveButton" )[ 0 ] ) ; sorter.on( Sorter.eventType.sorted, () => { - - $( "#result" ).empty().append( template.table( - { technologies : sorter.technologies.sorted - , criteria : sorter.criteria.all } ) ) ; + updateTable( 10 ) ; + downloader.updateCSV( sorter.technologies ) ; } ) ; } +function updateTable( longueur ) { + longueur = longueur ? longueur : sorter.technologies.sorted.length ; + $( "#result" ).empty().append( template.table( + { technologies : sorter.technologies.sorted.slice( 0, longueur ) + , criteria : sorter.criteria.all } ) ) ; +} + function loadControlPanel( mode ) { if( mode === "2Dimension" ) { load2DimensionControlPanel() ; diff --git a/public/javascripts/models/Criterion.mjs b/public/javascripts/models/Criterion.mjs index d82c413..fae9559 100644 --- a/public/javascripts/models/Criterion.mjs +++ b/public/javascripts/models/Criterion.mjs @@ -17,6 +17,7 @@ import { definePrivateProperties } from "../utils.mjs" ; * @property {Score} max - Maximum value for the criteria in the database * @property {number} weight - weight of the criteria for the score computation * @property {number} blurIntensity - [0-1] how much to extend the range [ evaluation - blurIntensity * ( max - min ), evaluation ] + *@property {string} sortingorder - indicates whether the criterion is ascending or descending * @memberof! Models * @alias module:Models~Criterion */ @@ -32,13 +33,14 @@ class Criterion extends EventEmitter { * @param {number} [serialization.max=5] - max value for the criteria, 5 if absent */ - constructor ( { name, description, min, max } ) { + constructor ( { name, description, min, max, sortingorder } ) { super( Criterion.eventType ) ; this.name = name ; this.description = description || name ; this.min = +min || 0 ; this.max = +max || 5 ; this.maxDominance = 0 ; + this.sortingorder = sortingorder ; definePrivateProperties( this, "_weight", "_blur" ) ; } diff --git a/public/javascripts/models/Technology.mjs b/public/javascripts/models/Technology.mjs index f087c71..e275fa4 100644 --- a/public/javascripts/models/Technology.mjs +++ b/public/javascripts/models/Technology.mjs @@ -11,6 +11,7 @@ * @property {Object.} bounds - blurred value for the criteria * @property {Object.} dominance - How many technologies are dominated ( value > bounds ) * @property {number} score - computed score : weighted sum. + * @todo Change everything to have a it in a one read ( compare this techno to an reduced array of technologies ) * @memberof! Models * @alias module:Models~Technology @@ -33,6 +34,7 @@ class Technology { this.evaluations = evaluations || {} ; this.bounds = {} ; this.dominance = {} ; + this.sortingorder = {} ; this.score = 0 ; } @@ -46,6 +48,7 @@ class Technology { for( const criterion of criteria ) { this.bounds[ criterion.name ] = criterion.blur( this.evaluations[ criterion.name ] ) ; this.dominance[ criterion.name ] = 0 ; + } return this ; } @@ -60,16 +63,20 @@ class Technology { */ updateDominance( criteria, technologies ) { for( const criterion of criteria ) { - this.dominance[ criterion.name ] = 0 ; - for( const technology of technologies ) { - if( technology !== this ) { - // this dominate technology - if( this.bounds[ criterion.name ] > technology.evaluations[ criterion.name ] ) this.dominance[ criterion.name ]++ ; - } - } - } + this.dominance[ criterion.name ] = 0 ; + for( const technology of technologies ) { + if( technology !== this ) { + // this dominate technology / ascending + if( criterion.sortingorder == 'ascending' && this.bounds[ criterion.name ] > technology.evaluations[ criterion.name ] ) this.dominance[ criterion.name ]++ ; + // descending + if( criterion.sortingorder == 'descending' && this.bounds[ criterion.name ] < technology.evaluations[ criterion.name ] ) this.dominance[ criterion.name ]++ ; + } + + } + } return this ; - } +} + /** diff --git a/public/stylesheets/_svg.scss b/public/stylesheets/_svg.scss index 25ddb3a..7084ee6 100644 --- a/public/stylesheets/_svg.scss +++ b/public/stylesheets/_svg.scss @@ -45,3 +45,4 @@ ellipse { .dropshadow tspan{ text-shadow: 1px 1px 0 rgba(255,255,255, 0.3); } + diff --git a/src/databaseConnector/sql/criteria_get_all.sql b/src/databaseConnector/sql/criteria_get_all.sql index e8f34f8..4aaca75 100644 --- a/src/databaseConnector/sql/criteria_get_all.sql +++ b/src/databaseConnector/sql/criteria_get_all.sql @@ -1,8 +1,11 @@ SELECT criteria.name , min( data.value ) AS min , max( data.value ) AS max - , description + , criteria.description + , sortingorder_name AS sortingorder FROM criteria - LEFT JOIN data ON data.criteria_name = criteria.name -WHERE type_name = 'numeric' -GROUP BY criteria.name + JOIN criteria_has_sortingorder ON criteria.name = criteria_has_sortingorder.criteria_name + LEFT JOIN data ON data.criteria_name = criteria.name +WHERE type_name LIKE 'numeric' +GROUP BY criteria.name + diff --git a/views/index.pug b/views/index.pug index 6e788b2..c054375 100644 --- a/views/index.pug +++ b/views/index.pug @@ -6,9 +6,10 @@ block content a.btn-floating.btn-large.waves-effect.waves-light img( src="/images/favicon/apple-touch-icon.png" width="100%") ul - li + li#saveButton a.btn-floating.yellow.darken-1 i.material-icons save + li a.btn-floating.red.sidenav-trigger(href="#" data-target="slide-out" ) i.material-icons menu diff --git a/views/layout.pug b/views/layout.pug index 384a4fd..df574fe 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -15,7 +15,7 @@ html body block content - + script( src="javascripts/papaparse.min.js") script( src="javascripts/jquery.min.js") script( src="javascripts/materialize.min.js") script( src="javascripts/capuchin.mjs" type="module" ) diff --git a/views/partials/table.pug b/views/partials/table.pug index 7763627..b642082 100644 --- a/views/partials/table.pug +++ b/views/partials/table.pug @@ -1,6 +1,6 @@ - function fixed2( x ) { return Number.parseFloat(x).toFixed(2)} - const names=["star","star_half","star_border"] -- function star( x ) { x = Math.max(0,x) ; const full = Math.floor( x * 5 ), half = Math.floor( x * 10 ) %2 ; return [ full, half, 5 - full - half ].map( (e,i)=> Array( e ).fill( names[i] ) ).flat() } +- function star( x ) { x = Math.min(1, Math.max(0,x)) ; const full = Math.floor( x * 5 ), half = Math.floor( x * 10 ) %2 ; return [ full, half, 5 - full - half ].map( (e,i)=> Array( e ).fill( names[i] ) ).flat() } table.responsive-table.striped.compactTable thead tr.hide-on-med-and-down