Skip to content
Snippets Groups Projects
Commit 7195f415 authored by Jean-Clement Gallardo's avatar Jean-Clement Gallardo
Browse files

Merge branch 'release/0.1.0'

parents 6cd12559 ac7e6f72
No related branches found
Tags 0.1.0
No related merge requests found
Pipeline #261387 passed
dist
node_modules
\ No newline at end of file
include:
- project: 'metabohub/web-components/mth-cicd'
ref: 1.0.3
file: '/templates/npm.gitlab-ci.yml'
.publish:
image: node:18
stage: deploy
before_script:
- apt-get update && apt-get install -y git default-jre
- npm install
- npm run build
# test:
# image: node:18
# stage: test
# before_script:
# - npm install
# script:
# - npm run test:unit
\ No newline at end of file
.npmrc 0 → 100644
@metabohub:registry=https://forgemia.inra.fr/api/v4/projects/${CI_PROJECT_ID}/packages/npm/
//forgemia.inra.fr/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}
\ No newline at end of file
# Vue 3 project template
This template includes :
- Vue 3
- [Vuetify 3](https://vuetifyjs.com/en/components/all/)
- Typescript
- ESlint
- npm packaging configuration
- [vitest](https://vitest.dev/guide/)
- [storybook](https://storybook.js.org/docs/vue/get-started/whats-a-story)
## Files to modify for the npm packaging
This template is configured to build and publish an npm package on GitLab, following this [tutorial](https://forgemia.inra.fr/metabohub/mth/-/wikis/mth2-wp5-t3/webcomponents-npm).
Replace 'ComponentExample' with the name of the component to publish in these files :
- lib/main.js
- package.json
- vite.config.ts
The package will be published with the GitLab CI.
## Use the package in another project
```sh
npm i -D @metabohub/component-example
```
If your project is not using vuetify, you need to import it in `src/main.ts` :
```ts
import { createApp } from 'vue'
import App from './App.vue'
import { vuetify } from "@metabohub/component-example";
createApp(App).use(vuetify).mount('#app')
```
Use the component :
```ts
<script setup lang="ts">
import { ComponentExample } from "@metabohub/component-example";
import "@metabohub/component-example/dist/style.css";
import type { SomeType } from "@metabohub/component-example";
</script>
<template>
<ComponentExample />
</template>
```
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-check
```sh
npm run type-check
```
### Compile for Production
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```
Check the coverage :
```sh
npm run coverage
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```
### View stories with [Storybook](https://storybook.js.org/docs/vue/get-started/whats-a-story)
```sh
npm run storybook
```
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
specPattern: "cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}",
baseUrl: "http://localhost:5173",
},
});
describe("Home", () => {
it("test", () => {
cy.visit("/")
})
})
\ No newline at end of file
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
\ No newline at end of file
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
/// <reference types="vite/client" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vuetify 3 Vite Preview</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
This diff is collapsed.
{
"name": "@metabohub/viz-flux",
"version": "0.0.0",
"private": false,
"description": "Vue component",
"scripts": {
"serve": "vite preview",
"build": "vite build",
"test:unit": "vitest --environment jsdom",
"coverage": "vitest run --coverage",
"test:e2e": "start-server-and-test dev http://localhost:5173 'cypress open'",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"dev": "vite",
"preview": "vite preview --port 4173",
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"main": "./dist/viz-flux.umd.js",
"module": "./dist/viz-flux.es.js",
"type": "module",
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/viz-flux.es.js",
"require": "./dist/viz-flux.umd.js"
},
"./dist/style.css": "./dist/style.css"
},
"types": "./dist/index.d.ts",
"dependencies": {
"@mdi/font": "^7.4.47",
"d3": "^7.9.0",
"roboto-fontface": "*",
"vue": "^3.5.12",
"vuetify": "^3.5.2",
"webfontloader": "^1.6.28"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.7.2",
"@storybook/addon-essentials": "^8.0.3",
"@storybook/addon-interactions": "^8.0.3",
"@storybook/addon-links": "^8.0.3",
"@storybook/blocks": "^8.0.3",
"@storybook/vue3": "^8.0.3",
"@storybook/vue3-vite": "^8.0.3",
"@types/d3": "^7.4.3",
"@types/jsdom": "^21.1.6",
"@types/node": "^20.11.14",
"@types/webfontloader": "^1.6.38",
"@vitejs/plugin-vue": "^5.0.3",
"@vitest/coverage-v8": "^1.2.2",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/test-utils": "^2.4.4",
"@vue/tsconfig": "^0.5.1",
"cypress": "^13.6.4",
"eslint": "^8.56.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-vue": "^9.21.1",
"jsdom": "^24.0.0",
"prettier": "^3.2.4",
"start-server-and-test": "^2.0.3",
"storybook": "^8.0.3",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-plugin-dts": "^3.7.3",
"vite-plugin-vuetify": "^2.0.1",
"vitest": "^1.2.2",
"vue-cli-plugin-vuetify": "^2.5.8",
"vue-tsc": "^1.8.27"
},
"license": "Apache-2.0"
}
public/favicon.ico

4.19 KiB

<template>
<v-app>
<v-main>
<FluxPanel
:draggable="true"
:network="network"
:graphStyleProperties="graphStyleProperties"
/>
</v-main>
</v-app>
</template>
<script setup lang="ts">
import FluxPanel from "./components/FluxPanel.vue";
import type { Network } from "@/types/Network";
const graphStyleProperties = {
linkStyles: {}
};
const network: Network = {
id: 'test data',
nodes: {
'A': {
label: 'Ava',
x: 100,
y: 100,
id: 'A',
hidden: false,
metadata: {}
},
'node150': {
label: 'Bot',
x: 400,
y: 550,
id: 'node150',
hidden: true,
metadata: {}
},
'C': {
label: 'Croa',
x: 800,
y: 750,
id: 'C',
hidden: true
}
},
links: [
{
id: 'A - B',
source: {
label: 'Ava',
x: 0,
y: 0,
id: 'A'
},
target: {
label: 'Bot',
x: 0,
y: 0,
id: 'node150'
},
classes: ['classicEdge']
},
{
id: 'B - C',
source: {
label: 'Bot',
x: 0,
y: 0,
id: 'node150'
},
target: {
label: 'Croa',
x: 0,
y: 0,
id: 'C'
},
classes: ['classicEdge']
}
]
}
</script>
<template>
<v-row :class="{ 'fluxCard': props.draggable }">
<v-col>
<v-card elevation="2" width="500">
<v-btn variant="plain" class="mt-1" :ripple="false" density="compact" style="float: right; cursor: pointer;"
icon="mdi-close-box-outline" @click="emit('closePanel')" />
<v-card-title :class="{ 'fluxStartDrag': props.draggable }">
Display flux data
</v-card-title>
<v-row>
<v-col cols="5" class="pr-0">
<v-card-text>
Load flux data file
</v-card-text>
</v-col>
<v-col class="pl-0 mr-4">
<v-file-input clearable multiple accept=".txt,.csv,.tab" width="250" label="File input" variant="underlined"
v-on:change="loadFiles"></v-file-input>
</v-col>
</v-row>
<v-table fixed-header max-height="470">
<thead>
<tr>
<th>
Parameters
</th>
<th>
Options
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>Select number of condition(s) to visualise</td>
<td>
<v-select
style="width: 200px;"
hide-details
v-model="panelProperties.nbCondition as string"
:value="panelProperties.nbCondition"
variant="outlined"
:items="['one', 'two']"
/>
</td>
<td></td>
</tr>
<tr>
<td>Select condition to visualise</td>
<td>
<v-select
style="width: 200px;"
hide-details
v-model="panelProperties.conditionSelected as string"
:value="fluxData.conditionsList[0]"
variant="outlined"
:items="fluxData.conditionsList"
:disabled="fluxData.conditionsList.length > 0 ? false : true"
/>
</td>
<td>
<input
v-model="panelProperties.conditionColor"
type="color"
/>
</td>
</tr>
<tr v-if="panelProperties.nbCondition === 'two'">
<td>Select second condition to visualise</td>
<td>
<v-select
style="width: 200px;"
hide-details
v-model="panelProperties.secondConditionSelected as string"
:value="fluxData.conditionsList[0]"
variant="outlined"
:items="fluxData.conditionsList"
:disabled="fluxData.conditionsList.length > 0 ? false : true"
/>
</td>
<td>
<input
v-model="panelProperties.secondConditionColor"
type="color"
/>
</td>
</tr>
<tr>
<td>Select scale to use</td>
<td>
<v-select
style="width: 200px;"
hide-details
v-model="panelProperties.scaleSelected as string"
:value="panelProperties.scaleSelected"
variant="outlined"
:items="['proportional', 'median scale']"
/>
</td>
<td></td>
</tr>
</tbody>
</v-table>
<div style="text-align: center;" class="mt-3">
<v-btn :disabled="panelProperties.display ? true : false" class="ma-2" @click="startFluxViz">Start</v-btn>
<v-btn :disabled="panelProperties.display ? false : true" class="ma-2" @click="refreshFluxViz">Refresh</v-btn>
<v-btn :disabled="panelProperties.display ? false : true" class="ma-2" @click="stopFluxViz">Stop</v-btn>
<v-btn :disabled="panelProperties.display ? false : true" class="ma-2" @click="panelProperties.showDataset = true">Show dataset</v-btn>
</div>
</v-card>
</v-col>
<v-col v-show="panelProperties.showDataset">
<v-card elevation="2" width="500">
<v-btn variant="plain" class="mt-1" :ripple="false" density="compact" style="float: right; cursor: pointer;"
icon="mdi-close-box-outline" @click="panelProperties.showDataset = false" />
<v-card-title>
Dataset informations
</v-card-title>
<svg id="datasetHistogram"></svg>
</v-card>
</v-col>
</v-row>
</template>
<script setup lang="ts">
// ------------------ Import ------------------
import { onMounted, PropType, reactive, watch } from "vue";
import { processData } from "@/composables/UseManageData";
import { startFluxVisualisation, startComparativeFluxVisualisation, removeFluxVisualisation, removeDatasetHistogram } from "@/composables/UseProcessStyle";
import type { Network } from "@/types/Network";
import { GraphStyleProperties } from "@/types/GraphStyleProperties";
// ------------------ Props ------------------
const props = defineProps({
draggable: {
type: Boolean,
default: false
},
x: {
type: Number,
default: 0
},
y: {
type: Number,
default: 0
},
network: {
type: Object as PropType<Network>,
default: {}
},
graphStyleProperties: {
type: Object as PropType<GraphStyleProperties>,
default: {}
}
});
const emit = defineEmits([
'closePanel'
]);
// ------------------ Reactive ------------------
const panelProperties = reactive({
nbCondition: 'one',
conditionSelected: '',
conditionColor: '#FF0000',
secondConditionSelected: '',
secondConditionColor: '#0000FF',
scaleSelected: 'median scale',
display: false,
showDataset: false
}) as {[key: string]: string | boolean};
const panelChoice = reactive({
displayMode: undefined as boolean | undefined,
firstFluxData: {},
secondFluxData: {}
});
const fluxData = reactive({
conditionsList: [] as Array<string>,
data: {} as {[key: string]: {[key: string]: {[key: string]: string}}}
});
// ------------------ Methods ------------------
function loadFiles(event: Event) {
const target = event.target as HTMLInputElement;
const files = target.files as FileList;
Object.keys(files).forEach((index: string) => {
const file = files[parseInt(index)];
const name = file.name;
const reader = new FileReader();
reader.onload = function () {
let data = reader.result as string;
fluxData.data[name] = processData(data);
Object.keys(fluxData.data[name]).forEach((condition: string) => {
if (condition !== 'id' && condition !== 'targetLabel') {
fluxData.conditionsList.push(name + ' -- ' + condition);
}
});
}
reader.readAsText(file);
});
}
function getFluxData(condition: string): {[key: string]: string} {
const conditionSplit = condition.split(' -- ');
const fileName = conditionSplit[0];
const conditionName = conditionSplit[1];
return fluxData.data[fileName][conditionName];
}
function startFluxViz() {
if (panelChoice.displayMode !== undefined) {
panelChoice.displayMode === true ? startFluxVisualisation(panelChoice.firstFluxData, props.network, props.graphStyleProperties, panelProperties.conditionColor as string, panelProperties.scaleSelected as string) :
startComparativeFluxVisualisation(panelChoice.firstFluxData, panelChoice.secondFluxData, props.network, props.graphStyleProperties, panelProperties.conditionColor as string, panelProperties.secondConditionColor as string, panelProperties.scaleSelected as string);
panelProperties.display = !panelProperties.display;
}
}
function refreshFluxViz() {
if (panelChoice.displayMode !== undefined) {
removeFluxVisualisation(props.network, props.graphStyleProperties);
removeDatasetHistogram();
panelChoice.displayMode === true ? startFluxVisualisation(panelChoice.firstFluxData, props.network, props.graphStyleProperties, panelProperties.conditionColor as string, panelProperties.scaleSelected as string) :
startComparativeFluxVisualisation(panelChoice.firstFluxData, panelChoice.secondFluxData, props.network, props.graphStyleProperties, panelProperties.conditionColor as string, panelProperties.secondConditionColor as string, panelProperties.scaleSelected as string);
}
}
function stopFluxViz() {
removeFluxVisualisation(props.network, props.graphStyleProperties);
removeDatasetHistogram();
panelProperties.display = !panelProperties.display;
panelProperties.showDataset = false;
}
// ------------------ Wathcers -------------------
watch(() => panelProperties, (newProperty: {[key: string]: string | boolean}) => {
if (newProperty.nbCondition === 'one' && newProperty.conditionSelected !== '') {
console.log(newProperty.conditionSelected);
panelChoice.firstFluxData = getFluxData(newProperty.conditionSelected as string);
panelChoice.displayMode = true;
} else if (newProperty.nbCondition === 'two' && newProperty.conditionSelected !== '' && newProperty.secondConditionSelected !== '') {
panelChoice.firstFluxData = getFluxData(newProperty.conditionSelected as string);
panelChoice.secondFluxData = getFluxData(newProperty.secondConditionSelected as string);
panelChoice.displayMode = false;
} else {
panelChoice.displayMode = undefined;
}
}, {deep: true});
// ------------------ onMounted ------------------
onMounted(() => {
if (props.draggable) {
// Add an event listener to make the card draggable according to props.draggable
const card: any = document.querySelector('.fluxCard');
const startDrag: any = document.querySelector('.fluxStartDrag');
card.style.top = props.y.toString() + 'px';
card.style.left = props.x.toString() + 'px';
if (startDrag) {
// Add style to card and card title
startDrag.style.cursor = 'grab';
card.style.position = 'absolute';
// Create mouse event to drag and drop
const handleMouseDown = (e: MouseEvent) => {
if (props.draggable) {
e.preventDefault();
const startX = e.clientX - card.getBoundingClientRect().left;
const startY = e.clientY - card.getBoundingClientRect().top;
const handleMouseMove = (e: MouseEvent) => {
const left = e.clientX - startX;
const top = e.clientY - startY;
card.style.left = left + 'px';
card.style.top = top + 'px';
};
const handleMouseUp = () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
}
};
// Add drag and drop function to card
startDrag.addEventListener('mousedown', handleMouseDown);
}
}
});
</script>
<style scoped>
.fluxCard {
transition: transform 0.2s, opacity 0.2s;
position: absolute;
}
</style>
\ No newline at end of file
export { default as FluxPanel } from "./FluxPanel.vue";
/**
* Takes the data from the input file and processes it to return formatted data
* @param rawData Data from input file
* @returns {Object} Process data
*/
export function processData(rawData: string) {
const data: {[key: string]: any} = {
id: [],
targetLabel: ''
};
const lines = rawData.split('\n');
const header = lines[0];
const headerSplit = header.split('\t');
data.targetLabel = headerSplit[0];
// Only ID map
if (headerSplit.length === 1) {
for (let i = 1; i < lines.length; i++) {
if (lines[i] !== "") {
data["id"].push(lines[i]);
}
}
}
// ID + data map
if (headerSplit.length > 1) {
const condList = [];
for (let j = 1; j < headerSplit.length; j++) {
condList.push(headerSplit[j]);
data[headerSplit[j]] = {};
}
for (let i = 1; i < lines.length; i++) {
if (lines[i] !== "") {
const line = lines[i].split('\t');
data["id"].push(line[0]);
for (let g = 0; g < condList.length; g++) {
data[condList[g]][line[0]] = line[g+1];
}
}
}
}
return data;
}
\ No newline at end of file
This diff is collapsed.
import { FluxPanel } from "@/components";
export { FluxPanel };
import { createApp } from "vue";
import App from "./App.vue";
import vuetify from "./plugins/vuetify";
import { loadFonts } from "./plugins/webfontloader";
loadFonts();
createApp(App).use(vuetify).mount("#app");
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