├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .vscode └── extensions.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TestingArena ├── ArenaVueUi3dBar.vue ├── ArenaVueUiAgePyramid.vue ├── ArenaVueUiBullet.vue ├── ArenaVueUiCandlestick.vue ├── ArenaVueUiCarouselTable.vue ├── ArenaVueUiChestnut.vue ├── ArenaVueUiCirclePack.vue ├── ArenaVueUiDonut.vue ├── ArenaVueUiDonutEvolution.vue ├── ArenaVueUiDumbbell.vue ├── ArenaVueUiFlow.vue ├── ArenaVueUiFunnel.vue ├── ArenaVueUiGalaxy.vue ├── ArenaVueUiGauge.vue ├── ArenaVueUiGizmo.vue ├── ArenaVueUiHeatmap.vue ├── ArenaVueUiHistoryPlot.vue ├── ArenaVueUiIcon.vue ├── ArenaVueUiKpi.vue ├── ArenaVueUiMolecule.vue ├── ArenaVueUiMoodRadar.vue ├── ArenaVueUiNestedDonuts.vue ├── ArenaVueUiOnion.vue ├── ArenaVueUiParallelCoordinatePlot.vue ├── ArenaVueUiQuadrant.vue ├── ArenaVueUiQuickChart.vue ├── ArenaVueUiRadar.vue ├── ArenaVueUiRating.vue ├── ArenaVueUiRelationCircle.vue ├── ArenaVueUiRings.vue ├── ArenaVueUiScatter.vue ├── ArenaVueUiSmiley.vue ├── ArenaVueUiSparkGauge.vue ├── ArenaVueUiSparkHistogram.vue ├── ArenaVueUiSparkStackbar.vue ├── ArenaVueUiSparkTrend.vue ├── ArenaVueUiSparkbar.vue ├── ArenaVueUiSparkline.vue ├── ArenaVueUiStackbar.vue ├── ArenaVueUiStripPlot.vue ├── ArenaVueUiTable.vue ├── ArenaVueUiTableHeatmap.vue ├── ArenaVueUiTableSparkline.vue ├── ArenaVueUiThermometer.vue ├── ArenaVueUiTimer.vue ├── ArenaVueUiTiremarks.vue ├── ArenaVueUiTreemap.vue ├── ArenaVueUiVerticalBar.vue ├── ArenaVueUiWaffle.vue ├── ArenaVueUiWheel.vue ├── ArenaVueUiWordCloud.vue ├── ArenaVueUiWorld.vue ├── ArenaVueUiXy.vue ├── ArenaVueUiXyCanvas.vue ├── Box.vue └── convertModel.js ├── add-dev-dep.cjs ├── cleanup.cjs ├── convert-json.cjs ├── copy-docs.cjs ├── copy-types.cjs ├── cypress.config.js ├── cypress ├── fixtures │ ├── index.js │ ├── rating.json │ ├── relation-circle.json │ ├── skeleton.json │ ├── smiley.json │ └── vdui-components.js └── support │ ├── commands.js │ ├── component-index.html │ ├── component.js │ └── index.js ├── del-dev-dep.cjs ├── documentation └── installation.md ├── index.html ├── jsconfig.js ├── llms.txt ├── package-lock.json ├── package.json ├── post-build.cjs ├── src ├── App.vue ├── Box.vue ├── SomeTest.vue ├── TestingArena.vue ├── atoms │ ├── Arrow.vue │ ├── BaseCheckbox.vue │ ├── BaseDirectionPad.cy.js │ ├── BaseDirectionPad.vue │ ├── BaseIcon.vue │ ├── ColorPicker.vue │ ├── DataTable.cy.js │ ├── DataTable.vue │ ├── Digit.vue │ ├── Legend.cy.js │ ├── Legend.vue │ ├── MonoSlicer.vue │ ├── NonSvgPenAndPaper.vue │ ├── PackageVersion.vue │ ├── PenAndPaper.cy.js │ ├── PenAndPaper.vue │ ├── RecursiveCircles.vue │ ├── RecursiveLabels.vue │ ├── RecursiveLinks.vue │ ├── Shape.cy.js │ ├── Shape.vue │ ├── Slicer.cy.js │ ├── Slicer.vue │ ├── SparkTooltip.vue │ ├── Title.cy.js │ ├── Title.vue │ ├── Tooltip.cy.js │ ├── Tooltip.vue │ ├── UserOptions.cy.js │ ├── UserOptions.vue │ └── vue-ui-pattern.vue ├── calcTooltipPosition.js ├── canvas-lib.js ├── chartDetector.js ├── components │ ├── vue-data-ui.cy.js │ ├── vue-data-ui.vue │ ├── vue-ui-3d-bar.cy.js │ ├── vue-ui-3d-bar.vue │ ├── vue-ui-accordion.vue │ ├── vue-ui-age-pyramid.cy.js │ ├── vue-ui-age-pyramid.vue │ ├── vue-ui-annotator.cy.js │ ├── vue-ui-annotator.vue │ ├── vue-ui-bullet.cy.js │ ├── vue-ui-bullet.vue │ ├── vue-ui-candlestick.cy.js │ ├── vue-ui-candlestick.vue │ ├── vue-ui-carousel-table.cy.js │ ├── vue-ui-carousel-table.vue │ ├── vue-ui-chestnut.cy.js │ ├── vue-ui-chestnut.vue │ ├── vue-ui-circle-pack.cy.js │ ├── vue-ui-circle-pack.vue │ ├── vue-ui-cursor.cy.js │ ├── vue-ui-cursor.vue │ ├── vue-ui-dashboard.vue │ ├── vue-ui-digits.cy.js │ ├── vue-ui-digits.vue │ ├── vue-ui-donut-evolution.cy.js │ ├── vue-ui-donut-evolution.vue │ ├── vue-ui-donut.cy.js │ ├── vue-ui-donut.vue │ ├── vue-ui-dumbbell.cy.js │ ├── vue-ui-dumbbell.vue │ ├── vue-ui-flow.cy.js │ ├── vue-ui-flow.vue │ ├── vue-ui-funnel.cy.js │ ├── vue-ui-funnel.vue │ ├── vue-ui-galaxy.cy.js │ ├── vue-ui-galaxy.vue │ ├── vue-ui-gauge.cy.js │ ├── vue-ui-gauge.vue │ ├── vue-ui-gizmo.cy.js │ ├── vue-ui-gizmo.vue │ ├── vue-ui-heatmap.cy.js │ ├── vue-ui-heatmap.vue │ ├── vue-ui-history-plot.cy.js │ ├── vue-ui-history-plot.vue │ ├── vue-ui-kpi.cy.js │ ├── vue-ui-kpi.vue │ ├── vue-ui-mini-loader.cy.js │ ├── vue-ui-mini-loader.vue │ ├── vue-ui-molecule.cy.js │ ├── vue-ui-molecule.vue │ ├── vue-ui-mood-radar.cy.js │ ├── vue-ui-mood-radar.vue │ ├── vue-ui-nested-donuts.cy.js │ ├── vue-ui-nested-donuts.vue │ ├── vue-ui-onion.cy.js │ ├── vue-ui-onion.vue │ ├── vue-ui-parallel-coordinate-plot.cy.js │ ├── vue-ui-parallel-coordinate-plot.vue │ ├── vue-ui-quadrant.cy.js │ ├── vue-ui-quadrant.vue │ ├── vue-ui-quick-chart.cy.js │ ├── vue-ui-quick-chart.vue │ ├── vue-ui-radar.cy.js │ ├── vue-ui-radar.vue │ ├── vue-ui-rating.cy.js │ ├── vue-ui-rating.vue │ ├── vue-ui-relation-circle.cy.js │ ├── vue-ui-relation-circle.vue │ ├── vue-ui-rings.cy.js │ ├── vue-ui-rings.vue │ ├── vue-ui-scatter.cy.js │ ├── vue-ui-scatter.vue │ ├── vue-ui-skeleton.cy.js │ ├── vue-ui-skeleton.vue │ ├── vue-ui-smiley.cy.js │ ├── vue-ui-smiley.vue │ ├── vue-ui-spark-trend.vue │ ├── vue-ui-sparkbar.cy.js │ ├── vue-ui-sparkbar.vue │ ├── vue-ui-sparkgauge.vue │ ├── vue-ui-sparkhistogram.cy.js │ ├── vue-ui-sparkhistogram.vue │ ├── vue-ui-sparkline.cy.js │ ├── vue-ui-sparkline.vue │ ├── vue-ui-sparkstackbar.cy.js │ ├── vue-ui-sparkstackbar.vue │ ├── vue-ui-stackbar.cy.js │ ├── vue-ui-stackbar.vue │ ├── vue-ui-strip-plot.cy.js │ ├── vue-ui-strip-plot.vue │ ├── vue-ui-table-heatmap.cy.js │ ├── vue-ui-table-heatmap.vue │ ├── vue-ui-table-sparkline.cy.js │ ├── vue-ui-table-sparkline.vue │ ├── vue-ui-table.vue │ ├── vue-ui-thermometer.cy.js │ ├── vue-ui-thermometer.vue │ ├── vue-ui-timer.vue │ ├── vue-ui-tiremarks.cy.js │ ├── vue-ui-tiremarks.vue │ ├── vue-ui-treemap.cy.js │ ├── vue-ui-treemap.vue │ ├── vue-ui-vertical-bar.cy.js │ ├── vue-ui-vertical-bar.vue │ ├── vue-ui-waffle.cy.js │ ├── vue-ui-waffle.vue │ ├── vue-ui-wheel.cy.js │ ├── vue-ui-wheel.vue │ ├── vue-ui-word-cloud.cy.js │ ├── vue-ui-word-cloud.vue │ ├── vue-ui-world.vue │ ├── vue-ui-xy-canvas.cy.js │ ├── vue-ui-xy-canvas.vue │ ├── vue-ui-xy.cy.js │ └── vue-ui-xy.vue ├── directives │ └── vClickOutside.js ├── dom-to-png.js ├── errors.json ├── event.js ├── exposedLib.js ├── geoProjections.js ├── getThemeConfig.js ├── getVueDataUiConfig.js ├── img.js ├── index.js ├── lib.js ├── main.js ├── packCircles.js ├── pdf.js ├── resources │ └── worldGeo.js ├── style.css ├── themes.json ├── timer.js ├── treemap.js ├── useArena.js ├── useChartAccessibility.js ├── useConfig.js ├── useMouse.js ├── useNestedProp.js ├── usePanZoom.js ├── usePatterns.js ├── usePrinter.js ├── useResponsive.js ├── useUserOptionState.js ├── vue-data-ui.css └── wordcloud.js ├── star.png ├── tests ├── chartDetector.test.js ├── exposedLib.test.js ├── getThemeConfig.test.js ├── getVueDataUiConfig.test.js ├── lib.test.js ├── treemap.test.js ├── useNestedProp.test.js └── wordcloud.test.js ├── types └── vue-data-ui.d.ts ├── vite.config.cypress.js ├── vite.config.js └── vue-data-ui-logo.png /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Vue Data UI version (please complete the following information):** 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | TODO.txt 26 | publish-build.cjs 27 | changeLog.cjs 28 | package-copy.json -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ALEC LLOYD PROBERT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TestingArena/ArenaVueUiGizmo.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | -------------------------------------------------------------------------------- /TestingArena/ArenaVueUiIcon.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /TestingArena/ArenaVueUiKpi.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | -------------------------------------------------------------------------------- /TestingArena/Box.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 60 | 61 | -------------------------------------------------------------------------------- /TestingArena/convertModel.js: -------------------------------------------------------------------------------- 1 | export default function convertArrayToObject(configArray) { 2 | const resultObject = {}; 3 | 4 | configArray.forEach(({ key, def }) => { 5 | const keys = key.split('.'); 6 | let currentObject = resultObject; 7 | 8 | keys.forEach((nestedKey, index) => { 9 | if (!currentObject.hasOwnProperty(nestedKey)) { 10 | if (index === keys.length - 1) { 11 | currentObject[nestedKey] = def; 12 | } else { 13 | currentObject[nestedKey] = {}; 14 | } 15 | } 16 | currentObject = currentObject[nestedKey]; 17 | }); 18 | }); 19 | 20 | return resultObject; 21 | } -------------------------------------------------------------------------------- /add-dev-dep.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | fs.readFile('package.json', 'utf8', (err, data) => { 4 | if (err) { 5 | console.error('Error reading package.json:', err); 6 | return; 7 | } 8 | let packageJson = JSON.parse(data); 9 | 10 | packageJson.devDependencies = { 11 | ...packageJson.devDependencies, 12 | "vue-data-ui": "file:../vue-data-ui" 13 | }; 14 | 15 | fs.writeFile('package.json', JSON.stringify(packageJson, null, 2), 'utf8', (err) => { 16 | if (err) { 17 | console.error('Error writing to package.json:', err); 18 | return; 19 | } 20 | console.log('-- DEV MODE : Local vue-data-ui package added successfully --'); 21 | }); 22 | }); -------------------------------------------------------------------------------- /cleanup.cjs: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const currentDir = path.dirname(require.main.filename); 5 | 6 | function deleteFolderRecursive(path) { 7 | if (fs.existsSync(path) && fs.lstatSync(path).isDirectory()) { 8 | fs.readdirSync(path).forEach(function (file, index) { 9 | let curPath = path + "/" + file; 10 | 11 | if (fs.lstatSync(curPath).isDirectory()) { // recurse 12 | deleteFolderRecursive(curPath); 13 | } else { // delete file 14 | fs.unlinkSync(curPath); 15 | } 16 | }); 17 | 18 | console.log(`Deleting directory "${path}"...`); 19 | fs.rmdirSync(path); 20 | } 21 | } 22 | 23 | deleteFolderRecursive(`${currentDir}\\node_modules\\vue-data-ui`); 24 | deleteFolderRecursive(`${currentDir}\\node_modules\\.vite`); 25 | deleteFolderRecursive('dist'); -------------------------------------------------------------------------------- /convert-json.cjs: -------------------------------------------------------------------------------- 1 | // convert-json.js 2 | const fs = require('fs'); 3 | const util = require('util'); 4 | 5 | /** 6 | * Converts a JSON file to a JS file that declares a const variable 7 | * and exports it as default, formatting as JS code. 8 | * 9 | * @example 10 | * // From the command line, run: 11 | * // $ node convert-json.cjs src/resources/worldGeo.json src/resources/worldGeo.js worldGeo 12 | * 13 | * // This will read 'src/resources/worldGeo.json', generate: 14 | * // const worldGeo = { ... }; 15 | * // export default worldGeo; 16 | * // and write it to 'src/resources/worldGeo.js' 17 | * 18 | * @param {string} inputJSONPath 19 | * @param {string} outputJSPath 20 | * @param {string} [variableName='data'] 21 | */ 22 | function convertJSON(inputJSONPath, outputJSPath, variableName = 'data') { 23 | const jsonContent = fs.readFileSync(inputJSONPath, 'utf-8'); 24 | const parsed = JSON.parse(jsonContent); 25 | 26 | const jsLikeString = util.inspect(parsed, { 27 | depth: null, 28 | compact: true, 29 | maxArrayLength: null, 30 | breakLength: 1000000, 31 | sorted: false, 32 | colors: false, 33 | quoteStyle: 'single', 34 | }); 35 | 36 | const jsContent = 37 | `const ${variableName} = ${jsLikeString};\n\nexport default ${variableName};\n`; 38 | 39 | fs.writeFileSync(outputJSPath, jsContent, 'utf-8'); 40 | } 41 | 42 | if (require.main === module) { 43 | const [, , inputJSONPath, outputJSPath, variableName = 'data'] = process.argv; 44 | 45 | if (!inputJSONPath || !outputJSPath) { 46 | console.log('Usage: node convert-json.js [variableName]'); 47 | process.exit(1); 48 | } 49 | 50 | convertJSON(inputJSONPath, outputJSPath, variableName); 51 | } 52 | 53 | module.exports = { convertJSON }; 54 | -------------------------------------------------------------------------------- /copy-docs.cjs: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | // Get the directory path of the current module 5 | const currentDir = path.dirname(require.main.filename); 6 | 7 | // Resolve the paths to the types and dist directories 8 | const typesDir = path.resolve(currentDir, "documentation"); 9 | const distDir = path.resolve(currentDir, "dist"); 10 | 11 | // Create dist directory if it doesn't exist 12 | if (!fs.existsSync(distDir)) { 13 | fs.mkdirSync(distDir); 14 | } 15 | 16 | // Resolve the path to the dist/types directory 17 | const distTypesDir = path.join(distDir, "documentation"); 18 | 19 | // Create dist/types directory if it doesn't exist 20 | if (!fs.existsSync(distTypesDir)) { 21 | fs.mkdirSync(distTypesDir); 22 | } 23 | 24 | // Copy .d.ts files from types directory to dist/types directory 25 | fs.readdirSync(typesDir).forEach((file) => { 26 | const srcFile = path.join(typesDir, file); 27 | const distFile = path.join(distTypesDir, file); 28 | 29 | fs.copyFileSync(srcFile, distFile); 30 | }); 31 | 32 | console.log("Doc directory copied successfully."); 33 | -------------------------------------------------------------------------------- /copy-types.cjs: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | // Get the directory path of the current module 5 | const currentDir = path.dirname(require.main.filename); 6 | 7 | // Resolve the paths to the types and dist directories 8 | const typesDir = path.resolve(currentDir, "types"); 9 | const distDir = path.resolve(currentDir, "dist"); 10 | 11 | // Create dist directory if it doesn't exist 12 | if (!fs.existsSync(distDir)) { 13 | fs.mkdirSync(distDir); 14 | } 15 | 16 | // Resolve the path to the dist/types directory 17 | const distTypesDir = path.join(distDir, "types"); 18 | 19 | // Create dist/types directory if it doesn't exist 20 | if (!fs.existsSync(distTypesDir)) { 21 | fs.mkdirSync(distTypesDir); 22 | } 23 | 24 | // Copy .d.ts files from types directory to dist/types directory 25 | fs.readdirSync(typesDir).forEach((file) => { 26 | const srcFile = path.join(typesDir, file); 27 | const distFile = path.join(distTypesDir, file); 28 | 29 | fs.copyFileSync(srcFile, distFile); 30 | fs.copyFileSync(srcFile, distFile.replace(/\.d\.ts$/, ".d.cts")); 31 | }); 32 | 33 | console.log("Types copied successfully."); 34 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | import fs from "fs"; 3 | import viteConfig from "./vite.config.cypress.js"; 4 | 5 | export default defineConfig({ 6 | component: { 7 | devServer: { 8 | framework: "vue", 9 | bundler: "vite", 10 | viteConfig 11 | }, 12 | setupNodeEvents(on, config) { 13 | on('task', { 14 | clearDownloads() { 15 | const downloadsFolder = 'cypress/downloads'; 16 | 17 | try { 18 | fs.readdirSync(downloadsFolder).forEach((file) => { 19 | const filePath = `${downloadsFolder}/${file}`; 20 | fs.unlinkSync(filePath); 21 | }); 22 | console.log('Downloads folder cleared.'); 23 | return null; 24 | } catch (err) { 25 | console.error(`Error clearing downloads folder: ${err}`); 26 | return err; 27 | } 28 | }, 29 | }); 30 | } 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /cypress/fixtures/index.js: -------------------------------------------------------------------------------- 1 | export function testCommonFeatures({ 2 | userOptions = false, 3 | title = false, 4 | subtitle = false, 5 | dataTable = false, 6 | tooltipCallback = false, 7 | legend = false, 8 | slicer = false 9 | }) { 10 | 11 | if (userOptions) { 12 | cy.get('[data-cy="user-options"]').should('exist').and('be.visible') 13 | } 14 | 15 | if (title) { 16 | cy.get('.atom-title').should('exist').and('be.visible').and('contain', 'Title') 17 | } 18 | 19 | if (subtitle) { 20 | cy.get('.atom-subtitle').should('exist').and('be.visible').and('contain', 'Subtitle') 21 | } 22 | 23 | if (dataTable) { 24 | cy.log('Open table') 25 | cy.get('[data-cy="user-options-summary"]').click() 26 | cy.get('[data-cy="user-options-table"]').should('exist').and('be.visible').click() 27 | cy.get('.atom-data-table').should('exist').and('be.visible') 28 | 29 | cy.log('close table') 30 | cy.get('[data-cy="data-table-close"]').should('exist').and('be.visible').click(); 31 | cy.get('.atom-data-table').should('not.be.visible') 32 | } 33 | 34 | if (tooltipCallback) { 35 | tooltipCallback(); 36 | cy.get('body').find('.vue-data-ui-tooltip').should('exist') 37 | } 38 | 39 | if (legend) { 40 | cy.get('.vue-data-ui-legend').should('exist').and('be.visible'); 41 | } 42 | 43 | if (slicer) { 44 | cy.get('[data-cy="slicer"]').should('exist').and('be.visible') 45 | } 46 | } -------------------------------------------------------------------------------- /cypress/fixtures/rating.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataset": { 3 | "rating": { 4 | "1": 1, 5 | "2": 1, 6 | "3": 1, 7 | "4": 1, 8 | "5": 30 9 | } 10 | }, 11 | "config": { 12 | "type": "star", 13 | "readonly": false, 14 | "from": 1, 15 | "to": 5, 16 | "style": { 17 | "fontFamily": "inherit", 18 | "animated": true, 19 | "itemSize": 32, 20 | "backgroundColor": "#FFFFFF", 21 | "star": { 22 | "activeColor": "#FFD055", 23 | "borderColor": "#FFD055", 24 | "borderWidth": 3, 25 | "apexes": 5, 26 | "inactiveColor": "#e1e5e8", 27 | "useGradient": true 28 | }, 29 | "image": { 30 | "src": "../../star.png", 31 | "inactiveOpacity": 0.3, 32 | "alt": "rating image" 33 | }, 34 | "title": { 35 | "textAlign": "center", 36 | "fontSize": 20, 37 | "color": "#2D353C", 38 | "bold": true, 39 | "text": "Title", 40 | "offsetY": 6, 41 | "subtitle": { 42 | "fontSize": 14, 43 | "color": "#CCCCCC", 44 | "bold": false, 45 | "text": "Subtitle", 46 | "offsetY": 12 47 | } 48 | }, 49 | "rating": { 50 | "show": true, 51 | "fontSize": 28, 52 | "bold": true, 53 | "roundingValue": 0, 54 | "position": "bottom", 55 | "offsetY": 0, 56 | "offsetX": 0 57 | }, 58 | "tooltip": { 59 | "show": true, 60 | "fontSize": 14, 61 | "offsetY": 0, 62 | "color": "#2D353C", 63 | "bold": true, 64 | "backgroundColor": "#FFFFFF", 65 | "borderColor": "#e1e5e8", 66 | "borderRadius": 4, 67 | "boxShadow": "0 6px 12px -6px rgba(0,0,0,0.2)" 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /cypress/fixtures/skeleton.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "type": "line", 4 | "style": { 5 | "backgroundColor": "#FFFFFF", 6 | "color": "#2D353C", 7 | "maxHeight": 500, 8 | "animated": true, 9 | "chestnut": { 10 | "color": "#6376DD" 11 | }, 12 | "sparkline": { 13 | "color": "#6376DD", 14 | "strokeWidth": 0.7 15 | }, 16 | "line": { 17 | "axis": { 18 | "show": true, 19 | "color": "#6376DD", 20 | "strokeWidth": 0.5 21 | }, 22 | "path": { 23 | "color": "#6376DD", 24 | "strokeWidth": 1, 25 | "showPlots": true 26 | } 27 | }, 28 | "bar": { 29 | "axis": { 30 | "show": true, 31 | "color": "#6376DD", 32 | "strokeWidth": 0.5 33 | }, 34 | "borderRadius": 0.5, 35 | "color": "#6376DD", 36 | "barWidth": 9 37 | }, 38 | "donut": { 39 | "color": "#6376DD", 40 | "strokeWidth": 64 41 | }, 42 | "onion": { 43 | "color": "#6376DD" 44 | }, 45 | "gauge": { 46 | "color": "#6376DD" 47 | }, 48 | "quadrant": { 49 | "grid": { 50 | "color": "#6376DD", 51 | "strokeWidth": 0.5 52 | }, 53 | "plots": { 54 | "radius": 1.5, 55 | "color": "#6376DD" 56 | } 57 | }, 58 | "radar": { 59 | "grid": { 60 | "color": "#6376DD", 61 | "strokeWidth": 0.5 62 | }, 63 | "shapes": { 64 | "color": "#6376DD" 65 | } 66 | }, 67 | "waffle": { 68 | "color": "#6376DD" 69 | }, 70 | "table": { 71 | "th": { 72 | "color": "#6376DD" 73 | }, 74 | "td": { 75 | "color": "#6376DD", 76 | "strokeWidth": 0.5 77 | } 78 | }, 79 | "rating": { 80 | "useSmiley": false, 81 | "color": "#6376DD", 82 | "filled": true, 83 | "strokeWidth": 1, 84 | "maxWidth": 200 85 | }, 86 | "verticalBar": { 87 | "axis": { 88 | "show": true, 89 | "color": "#6376DD", 90 | "strokeWidth": 0.5 91 | }, 92 | "borderRadius": 0.5, 93 | "color": "#6376DD" 94 | }, 95 | "heatmap": { 96 | "cellsX": 26, 97 | "cellsY": 7, 98 | "color": "#6376DD" 99 | }, 100 | "candlesticks": { 101 | "axis": { 102 | "show": true, 103 | "color": "#6376DD", 104 | "strokeWidth": 0.5 105 | }, 106 | "candle": { 107 | "color": "#6376DD", 108 | "strokeWidth": 1 109 | } 110 | }, 111 | "pyramid": { 112 | "color": "#6376DD" 113 | } 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /cypress/fixtures/smiley.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataset": { 3 | "rating": { 4 | "1": 1, 5 | "2": 1, 6 | "3": 1, 7 | "4": 1, 8 | "5": 30 9 | } 10 | }, 11 | "config": { 12 | "readonly": false, 13 | "style": { 14 | "fontFamily": "inherit", 15 | "itemSize": 32, 16 | "backgroundColor": "#FFFFFF", 17 | "colors": { 18 | "activeReadonly": ["#e20001", "#ff9f03", "#ffd004", "#61c900", "#059f00"], 19 | "active": ["#e20001", "#ff9f03", "#ffd004", "#61c900", "#059f00"], 20 | "inactive": ["#e1e5e8","#e1e5e8","#e1e5e8","#e1e5e8","#e1e5e8"] 21 | }, 22 | "icons": { 23 | "filled": false, 24 | "useGradient": true 25 | }, 26 | "title": { 27 | "textAlign": "center", 28 | "fontSize": 20, 29 | "color": "#2D353C", 30 | "bold": true, 31 | "text": "Title", 32 | "offsetY": 6, 33 | "subtitle": { 34 | "fontSize": 14, 35 | "color": "#CCCCCC", 36 | "bold": false, 37 | "text": "Subtitle", 38 | "offsetY": 12 39 | } 40 | }, 41 | "rating": { 42 | "show": true, 43 | "fontSize": 28, 44 | "bold": true, 45 | "roundingValue": 0, 46 | "position": "bottom", 47 | "offsetY": 0, 48 | "offsetX":0 49 | }, 50 | "tooltip": { 51 | "show": true, 52 | "fontSize": 14, 53 | "offsetY": 0, 54 | "color":"#2D353C", 55 | "bold": true, 56 | "backgroundColor":"#FFFFFF", 57 | "borderColor": "#e1e5e8", 58 | "borderRadius": 4, 59 | "boxShadow":"0 6px 12px -6px rgba(0,0,0,0.2)" 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | 27 | Cypress.Commands.add('clearDownloads', () => { 28 | cy.task('clearDownloads'); 29 | }); -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /cypress/support/component.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/vue' 23 | 24 | Cypress.Commands.add('mount', mount) 25 | 26 | Cypress.on("uncaught:exception", (err) => { 27 | if (err.message.includes("ResizeObserver loop completed with undelivered notifications")) { 28 | return false; 29 | } 30 | }); -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | import '../plugins'; -------------------------------------------------------------------------------- /del-dev-dep.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | function removeKey(obj, key) { 4 | const { [key]: omit, ...rest } = obj; 5 | return rest; 6 | } 7 | fs.readFile('package.json', 'utf8', (err, data) => { 8 | if (err) { 9 | console.error('Error reading package.json:', err); 10 | return; 11 | } 12 | 13 | let packageJson = JSON.parse(data); 14 | 15 | const keyToRemove = "vue-data-ui"; 16 | 17 | if (packageJson.devDependencies && packageJson.devDependencies[keyToRemove]) { 18 | packageJson.devDependencies = removeKey(packageJson.devDependencies, keyToRemove); 19 | 20 | fs.writeFile('package.json', JSON.stringify(packageJson, null, 2), 'utf8', (err) => { 21 | if (err) { 22 | console.error('Error writing to package.json:', err); 23 | return; 24 | } 25 | console.log('-- BUILD : Removed local vue-data-ui dev dependency before build --'); 26 | }); 27 | } else { 28 | console.log(`Key '${keyToRemove}' not found in devDependencies.`); 29 | } 30 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Vue 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /jsconfig.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphieros/vue-data-ui/9f13dde454ea583b09396403eec7f913d90e5e96/jsconfig.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-data-ui", 3 | "private": false, 4 | "version": "2.9.6", 5 | "type": "module", 6 | "description": "A user-empowering data visualization Vue 3 components library for eloquent data storytelling", 7 | "keywords": [ 8 | "3d bar", 9 | "Vue", 10 | "accelerometer", 11 | "age pyramid", 12 | "annotator", 13 | "candlestick", 14 | "chart", 15 | "cluster", 16 | "dashboard", 17 | "data storytelling", 18 | "data visualization", 19 | "donut evolution", 20 | "donut", 21 | "dumbbell", 22 | "funnel", 23 | "galaxy", 24 | "gauge", 25 | "graph", 26 | "heatmap", 27 | "kpi", 28 | "line", 29 | "molecule", 30 | "mood radar", 31 | "quadrant", 32 | "quick chart", 33 | "radar", 34 | "rating", 35 | "relationship circle", 36 | "rings", 37 | "scatter", 38 | "screenshot", 39 | "skeleton", 40 | "smiley", 41 | "sparkbar", 42 | "sparkline", 43 | "stackbar", 44 | "table heatmap", 45 | "table", 46 | "thermometer", 47 | "tiremarks", 48 | "tree", 49 | "treemap", 50 | "waffle", 51 | "wheel", 52 | "wordcloud", 53 | "circle packing" 54 | ], 55 | "author": "Alec Lloyd Probert", 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/graphieros/vue-data-ui.git" 59 | }, 60 | "homepage": "https://vue-data-ui.graphieros.com/", 61 | "license": "MIT", 62 | "files": [ 63 | "dist" 64 | ], 65 | "exports": { 66 | ".": { 67 | "import": { 68 | "types": "./dist/types/vue-data-ui.d.ts", 69 | "default": "./dist/vue-data-ui.js" 70 | }, 71 | "default": { 72 | "types": "./dist/types/vue-data-ui.d.cts", 73 | "default": "./dist/vue-data-ui.cjs" 74 | } 75 | }, 76 | "./style.css": "./dist/style.css" 77 | }, 78 | "main": "dist/vue-data-ui.cjs", 79 | "module": "dist/vue-data-ui.js", 80 | "types": "dist/types/vue-data-ui.d.ts", 81 | "scripts": { 82 | "dev": "node add-dev-dep.cjs && npm i && vite", 83 | "clean": "node cleanup.cjs", 84 | "build": "npm run clean && vite build --mode production && node copy-types.cjs && npm i", 85 | "prod": "node del-dev-dep.cjs && npm run test && npx cypress run --component && npm run clean && vite build --mode production && node copy-types.cjs && node copy-docs.cjs && node post-build.cjs", 86 | "prod:publish": "npm run prod && node publish-build.cjs", 87 | "build:dev": "npm run build && npm run dev", 88 | "test": "vitest --run", 89 | "test:w": "vitest --watch", 90 | "test:e2e": "npx cypress open", 91 | "simple-build": "npm run clean && vite build --mode production && node copy-types.cjs", 92 | "preprod": "node del-dev-dep.cjs && npm run clean && vite build --mode production && node copy-types.cjs && node copy-docs.cjs && node post-build.cjs" 93 | }, 94 | "peerDependencies": { 95 | "vue": ">=3.3.0", 96 | "jspdf": "^3.0.1" 97 | }, 98 | "peerDependenciesMeta": { 99 | "jspdf": { 100 | "optional": true 101 | } 102 | }, 103 | "devDependencies": { 104 | "@vitejs/plugin-vue": "^5.2.3", 105 | "cypress": "^14.0.3", 106 | "remove-attr": "^0.0.13", 107 | "sass": "^1.57.1", 108 | "simple-git": "^3.24.0", 109 | "vite": "^6.3.5", 110 | "vitest": "^3.1.1", 111 | "vue": "^3.5.14" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /post-build.cjs: -------------------------------------------------------------------------------- 1 | const { renameSync } = require("node:fs"); 2 | const { resolve } = require("path"); 3 | 4 | const oldName = resolve(__dirname, "dist/vue-data-ui.css"); 5 | const newName = resolve(__dirname, "dist/style.css"); 6 | 7 | try { 8 | renameSync(oldName, newName); 9 | console.log(`Renamed '${oldName}' to '${newName}' successfully!`); 10 | } catch (error) { 11 | console.error(`Error renaming file: ${error.message}`); 12 | } -------------------------------------------------------------------------------- /src/SomeTest.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /src/atoms/Arrow.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | -------------------------------------------------------------------------------- /src/atoms/BaseCheckbox.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /src/atoms/BaseDirectionPad.cy.js: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref } from "vue"; 2 | import BaseDirectionPad from "./BaseDirectionPad.vue"; 3 | 4 | describe('', () => { 5 | it('renders correctly with default props', () => { 6 | cy.mount(defineComponent({ 7 | components: { BaseDirectionPad }, 8 | template: '', 9 | })); 10 | cy.get('button').should('have.length', 5); 11 | cy.get('button').first().should('have.css', 'position', 'absolute'); 12 | cy.get('button').first().should('have.css', 'left', '0px'); 13 | cy.get('[data-cy="base-icon"]').each(icon => { 14 | cy.wrap(icon).as('icon') 15 | cy.get('@icon').find('path').eq(0).should($path => { 16 | const stroke = $path.attr('stroke'); 17 | const fill = $path.attr('fill'); 18 | if (stroke !== 'none') { 19 | expect(stroke).to.eq('#1A1A1A'); 20 | } else if (fill) { 21 | expect(fill).to.eq('#1A1A1A'); 22 | } 23 | }); 24 | }); 25 | 26 | }); 27 | 28 | it('emits', () => { 29 | const moveLeft = cy.stub(); 30 | const moveTop = cy.stub(); 31 | const moveRight = cy.stub(); 32 | const moveBottom = cy.stub(); 33 | const reset = cy.stub(); 34 | 35 | cy.mount(defineComponent({ 36 | components: { BaseDirectionPad }, 37 | setup() { 38 | return { moveLeft, moveTop, moveRight, moveBottom, reset }; 39 | }, 40 | template: ` 41 | 48 | ` 49 | })); 50 | cy.get('[data-cy="direction-pad-left"]').click(); 51 | cy.wrap(moveLeft).should('have.been.calledOnce'); 52 | cy.get('[data-cy="direction-pad-top"]').click(); 53 | cy.wrap(moveTop).should('have.been.calledOnce'); 54 | cy.get('[data-cy="direction-pad-right"]').click(); 55 | cy.wrap(moveRight).should('have.been.calledOnce'); 56 | cy.get('[data-cy="direction-pad-bottom"]').click(); 57 | cy.wrap(moveBottom).should('have.been.calledOnce'); 58 | cy.get('[data-cy="direction-pad-reset"]').click(); 59 | cy.wrap(reset).should('have.been.calledOnce'); 60 | }); 61 | 62 | it('handles color prop', () => { 63 | cy.mount(defineComponent({ 64 | components: { BaseDirectionPad }, 65 | setup() { 66 | const color = ref('#FF0000'); 67 | return { color }; 68 | }, 69 | template: ` 70 | 71 | ` 72 | })); 73 | 74 | cy.get('[data-cy="base-icon"]').each(icon => { 75 | cy.wrap(icon).as('icon') 76 | cy.get('@icon').find('path').eq(0).should($path => { 77 | const stroke = $path.attr('stroke'); 78 | const fill = $path.attr('fill'); 79 | if (stroke !== 'none') { 80 | expect(stroke).to.eq('#FF0000'); 81 | } else if (fill) { 82 | expect(fill).to.eq('#FF0000'); 83 | } 84 | }); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/atoms/BaseDirectionPad.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /src/atoms/DataTable.cy.js: -------------------------------------------------------------------------------- 1 | import { defineComponent, reactive, ref } from 'vue'; 2 | import DataTable from './DataTable.vue'; 3 | 4 | describe('', () => { 5 | it('renders correctly with slots', () => { 6 | cy.viewport(800, 500) 7 | cy.mount(defineComponent({ 8 | components: { DataTable }, 9 | setup() { 10 | const colNames = reactive(["Column 1", "Column 2", "Column 3"]); 11 | const head = reactive([ 12 | { name: "Header 1", color: "#FF0000" }, 13 | { name: "Header 2", color: "#00FF00" }, 14 | { name: "Header 3", color: "#0000FF" }, 15 | ]); 16 | const body = reactive([ 17 | [{ name: "Row 1 - Col 1" }, { name: "Row 1 - Col 2" }, { name: "Row 1 - Col 3" }], 18 | [{ name: "Row 2 - Col 1" }, { name: "Row 2 - Col 2" }, { name: "Row 2 - Col 3" }], 19 | ]); 20 | const config = reactive({ 21 | th: { backgroundColor: "#f0f0f0", color: "#000", outline: "1px solid #ccc" }, 22 | td: { backgroundColor: "#fff", color: "#333", outline: "1px solid #ddd" }, 23 | shape: "circle", 24 | breakpoint: 600, 25 | }); 26 | 27 | const closeEmitted = ref(false); 28 | 29 | return { colNames, head, body, config, closeEmitted }; 30 | }, 31 | template: ` 32 | 40 | 43 | 46 | 47 | ` 48 | })); 49 | 50 | cy.get('[data-cy="vue-data-ui-table-data"]').should('exist'); 51 | cy.get('caption').should('contain', 'Test Table'); 52 | cy.get('[data-cy="th"]').should('have.length', 3); 53 | cy.get('[data-cy="th"]').first().should('contain', 'Header 1'); 54 | cy.get('[data-cy="td"]').should('have.length', 6); 55 | cy.get('[data-cy="td"]').first().should('contain', 'Row 1 - Col 1'); 56 | cy.get('[role="button"]').click(); 57 | cy.get('[data-cy="th"]').should('exist'); 58 | 59 | cy.log('Responsive mode') 60 | cy.viewport(500, 350); 61 | cy.get('th').should('not.be.visible'); 62 | cy.get('td').first().should('have.attr', 'data-cell', 'Column 1'); 63 | 64 | cy.log('close') 65 | cy.get('[data-cy="data-table-close"]').click() 66 | cy.wrap(null).then(() => { 67 | expect(Cypress.vueWrapper.vm.closeEmitted).to.be.true; 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/atoms/Digit.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | -------------------------------------------------------------------------------- /src/atoms/Legend.cy.js: -------------------------------------------------------------------------------- 1 | import { defineComponent, reactive } from 'vue'; 2 | import Legend from './Legend.vue'; 3 | 4 | describe('', () => { 5 | 6 | const legend = [ 7 | { 8 | name: 'circle', 9 | color: '#F00000', 10 | value: 1, 11 | shape: 'circle', 12 | }, 13 | { 14 | name: 'triangle', 15 | color: '#00FF00', 16 | value: 2, 17 | shape: 'triangle', 18 | }, 19 | { 20 | name: 'square', 21 | color: '#0000FF', 22 | value: 3, 23 | shape: 'square', 24 | }, 25 | { 26 | name: 'diamond', 27 | color: '#F0FF00', 28 | value: 4, 29 | shape: 'diamond', 30 | }, 31 | { 32 | name: 'pentagon', 33 | color: '#F000F0', 34 | value: 6, 35 | shape: 'pentagon', 36 | }, 37 | { 38 | name: 'hexagon', 39 | color: '#FF99AA', 40 | value: 7, 41 | shape: 'hexagon', 42 | }, 43 | { 44 | name: 'star', 45 | color: '#66AA66', 46 | value: 8, 47 | shape: 'star', 48 | }, 49 | ]; 50 | 51 | it('renders correctly with slots', () => { 52 | cy.mount(defineComponent({ 53 | components: { Legend }, 54 | setup() { 55 | const legendSet = legend 56 | 57 | const config = reactive({ 58 | backgroundColor: '#fff', 59 | fontSize: 14, 60 | color: '#333', 61 | paddingBottom: 12, 62 | cy: 'legend-container' 63 | }); 64 | 65 | return { legendSet, config }; 66 | }, 67 | template: ` 68 | 69 | 72 | 75 | 76 | ` 77 | })); 78 | 79 | cy.get('[data-cy="legend-title"]').should('exist').and('contain', 'Legend Title'); 80 | cy.get('[data-cy="legend-item"]').as('items').should('have.length', 7); 81 | cy.get('@items').each((item, i) => { 82 | cy.wrap(item).as('item') 83 | cy.get('@item').should('contain', `${legend[i].name} - value:${legend[i].value}`) 84 | }) 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/atoms/Legend.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 60 | 61 | -------------------------------------------------------------------------------- /src/atoms/PackageVersion.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/atoms/RecursiveCircles.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 101 | -------------------------------------------------------------------------------- /src/atoms/RecursiveLabels.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 58 | -------------------------------------------------------------------------------- /src/atoms/RecursiveLinks.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 65 | 66 | -------------------------------------------------------------------------------- /src/atoms/Shape.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | -------------------------------------------------------------------------------- /src/atoms/SparkTooltip.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 95 | 96 | -------------------------------------------------------------------------------- /src/atoms/Title.cy.js: -------------------------------------------------------------------------------- 1 | import Title from './Title.vue' 2 | 3 | describe('', () => { 4 | it('renders content with default styles', () => { 5 | cy.viewport(100, 60) 6 | cy.mount(Title, { 7 | props: { 8 | config: { 9 | title: { 10 | cy: 'title', 11 | text: 'Title', 12 | color: '#FF0000' 13 | }, 14 | subtitle: { 15 | cy: 'subtitle', 16 | text: 'Subtitle', 17 | color: '#CCCCCC', 18 | } 19 | } 20 | } 21 | }) 22 | cy.get('[data-cy="title"]').then(($title) => { 23 | cy.wrap($title).should('exist').and('be.visible') 24 | cy.wrap($title).should('have.css', 'font-weight', '700') 25 | cy.wrap($title).should('have.css', 'color', 'rgb(255, 0, 0)') 26 | cy.wrap($title).should('have.css', 'font-size', '20px') 27 | cy.contains('Title') 28 | }) 29 | 30 | cy.get('[data-cy="subtitle"]').then(($subtitle) => { 31 | cy.wrap($subtitle).should('exist').and('be.visible') 32 | cy.wrap($subtitle).should('have.css', 'font-weight', '400') 33 | cy.wrap($subtitle).should('have.css', 'color', 'rgb(204, 204, 204)') 34 | cy.wrap($subtitle).should('have.css', 'font-size', '14px') 35 | cy.contains('Subtitle') 36 | }) 37 | }) 38 | 39 | it('renders content with custom styles', () => { 40 | cy.viewport(100, 70) 41 | cy.mount(Title, { 42 | props: { 43 | config: { 44 | title: { 45 | cy: 'title', 46 | text: 'Title', 47 | color: '#FF0000', 48 | bold: false, 49 | fontSize: 24 50 | }, 51 | subtitle: { 52 | cy: 'subtitle', 53 | text: 'Subtitle', 54 | color: '#CCCCCC', 55 | bold: true, 56 | fontSize: 16 57 | } 58 | } 59 | } 60 | }) 61 | cy.get('[data-cy="title"]').then(($title) => { 62 | cy.wrap($title).should('exist').and('be.visible') 63 | cy.wrap($title).should('have.css', 'font-weight', '400') 64 | cy.wrap($title).should('have.css', 'color', 'rgb(255, 0, 0)') 65 | cy.wrap($title).should('have.css', 'font-size', '24px') 66 | cy.contains('Title') 67 | }) 68 | 69 | cy.get('[data-cy="subtitle"]').then(($subtitle) => { 70 | cy.wrap($subtitle).should('exist').and('be.visible') 71 | cy.wrap($subtitle).should('have.css', 'font-weight', '700') 72 | cy.wrap($subtitle).should('have.css', 'color', 'rgb(204, 204, 204)') 73 | cy.wrap($subtitle).should('have.css', 'font-size', '16px') 74 | cy.contains('Subtitle') 75 | }) 76 | }) 77 | }) -------------------------------------------------------------------------------- /src/atoms/Title.vue: -------------------------------------------------------------------------------- 1 | <script setup> 2 | import { useNestedProp } from "../useNestedProp"; 3 | 4 | const props = defineProps({ 5 | config: { 6 | type: Object, 7 | default() { 8 | return {} 9 | } 10 | }, 11 | lineHeight: { 12 | type: [String, Boolean], 13 | default: false 14 | } 15 | }); 16 | 17 | const CONFIG = useNestedProp({ 18 | userConfig: props.config, 19 | defaultConfig: { 20 | title: { 21 | cy: "", 22 | text: "", 23 | color: "", 24 | fontSize: 20, 25 | bold: true, 26 | textAlign: 'center', 27 | paddingLeft: 0, 28 | paddingRight: 0 29 | }, 30 | subtitle: { 31 | cy: "", 32 | text: "", 33 | color: "", 34 | fontSize: 14, 35 | bold: false 36 | }, 37 | } 38 | }); 39 | 40 | </script> 41 | 42 | <template> 43 | <div 44 | class="atom-title" 45 | :data-cy="CONFIG.title.cy" 46 | :style="`width: calc(100% - ${CONFIG.title.paddingLeft + CONFIG.title.paddingRight}px); text-align:${CONFIG.title.textAlign};color:${ 47 | CONFIG.title.color 48 | };font-size:${CONFIG.title.fontSize}px;font-weight:${ 49 | CONFIG.title.bold ? 'bold' : '' 50 | };padding-left:${CONFIG.title.paddingLeft}px;padding-right:${CONFIG.title.paddingRight}px;${lineHeight ? `line-height:${lineHeight}`: ''}`" 51 | > 52 | {{ CONFIG.title.text }} 53 | </div> 54 | <div 55 | class="atom-subtitle" 56 | :data-cy="CONFIG.subtitle.cy" 57 | v-if="CONFIG.subtitle.text" 58 | :style="`width: calc(100% - ${CONFIG.title.paddingLeft + CONFIG.title.paddingRight}px); text-align:${CONFIG.title.textAlign};color:${ 59 | CONFIG.subtitle.color 60 | };font-size:${CONFIG.subtitle.fontSize}px;font-weight:${ 61 | CONFIG.subtitle.bold ? 'bold' : '' 62 | };padding-left:${CONFIG.title.paddingLeft}px;padding-right:${CONFIG.title.paddingRight}px;${lineHeight ? `line-height:${lineHeight}`: ''}`" 63 | > 64 | {{ CONFIG.subtitle.text }} 65 | </div> 66 | <div 67 | :data-cy="CONFIG.subtitle.cy" 68 | v-if="CONFIG.subtitle.text" 69 | :style="`width: calc(100% - ${CONFIG.title.paddingLeft + CONFIG.title.paddingRight}px); text-align:${CONFIG.title.textAlign};color:${ 70 | CONFIG.subtitle.color 71 | };font-size:${CONFIG.subtitle.fontSize}px;font-weight:${ 72 | CONFIG.subtitle.bold ? 'bold' : '' 73 | };padding-left:${CONFIG.title.paddingLeft}px;padding-right:${CONFIG.title.paddingRight}px;${lineHeight ? `line-height:${lineHeight}`: ''}`" 74 | > 75 | <slot/> 76 | </div> 77 | </template> 78 | -------------------------------------------------------------------------------- /src/atoms/Tooltip.cy.js: -------------------------------------------------------------------------------- 1 | import Tooltip from './Tooltip.vue' 2 | 3 | describe('<Tooltip />', () => { 4 | 5 | beforeEach(() => { 6 | cy.mount(Tooltip, { 7 | props: { 8 | content: `<div data-cy="tooltip-content">Content</div>`, 9 | show: true, 10 | }, 11 | slots: { 12 | default: { 13 | render: () => 'Default slot' 14 | }, 15 | ['tooltip-before']: { 16 | render: () => 'Slot before' 17 | }, 18 | ['tooltip-after']: { 19 | render: () => 'Slot after' 20 | }, 21 | } 22 | }) 23 | }) 24 | 25 | it('renders all contents', () => { 26 | cy.contains('Default slot') 27 | cy.contains('Content') 28 | cy.contains('Slot before') 29 | cy.contains('Slot after') 30 | }) 31 | 32 | it('follows the mouse', () => { 33 | cy.get('body').trigger('mousemove', { clientX: 200, clientY: 200, force: true}) 34 | cy.get('[data-cy="tooltip"]').should('have.css', 'top', '224px') 35 | cy.get('[data-cy="tooltip"]').should('have.css', 'left', '200px') 36 | }) 37 | }) -------------------------------------------------------------------------------- /src/atoms/Tooltip.vue: -------------------------------------------------------------------------------- 1 | <script setup> 2 | import { computed, ref } from "vue"; 3 | import { calcTooltipPosition } from "../calcTooltipPosition"; 4 | import { useMouse } from "../useMouse"; 5 | import { opacity, setOpacity } from "../lib"; 6 | 7 | const props = defineProps({ 8 | backgroundColor: { 9 | type: String, 10 | default: "#FFFFFF" 11 | }, 12 | color: { 13 | type: String, 14 | default: "#000000" 15 | }, 16 | content: String, 17 | maxWidth: { 18 | type: String, 19 | default: '300px' 20 | }, 21 | parent: { 22 | type: Object 23 | }, 24 | show: { 25 | type: Boolean, 26 | default: false, 27 | }, 28 | isCustom: { 29 | type: Boolean, 30 | default: false, 31 | }, 32 | fontSize: { 33 | type: [Number, String], 34 | default: 14 35 | }, 36 | borderRadius: { 37 | type: Number, 38 | default: 4 39 | }, 40 | borderColor: { 41 | type: String, 42 | default: '#e1e5e8' 43 | }, 44 | borderWidth: { 45 | type: Number, 46 | default: 1 47 | }, 48 | backgroundOpacity: { 49 | type: Number, 50 | default: 100, 51 | }, 52 | position: { 53 | type: String, 54 | default: "center" 55 | }, 56 | offsetY: { 57 | type: Number, 58 | default: 24 59 | }, 60 | blockShiftY: { 61 | type: Boolean, 62 | default: false, 63 | }, 64 | isFullscreen: { 65 | type: Boolean, 66 | default: false 67 | } 68 | }); 69 | 70 | const tooltip = ref(null) 71 | 72 | const clientPosition = ref(useMouse(props.parent)); 73 | 74 | const position = computed(() => { 75 | return calcTooltipPosition({ 76 | tooltip: tooltip.value, 77 | chart: props.parent, 78 | clientPosition: clientPosition.value, 79 | positionPreference: props.position, 80 | defaultOffsetY: props.offsetY, 81 | blockShiftY: props.blockShiftY 82 | }); 83 | }) 84 | 85 | const convertedBackground = computed(() => { 86 | return setOpacity(props.backgroundColor, props.backgroundOpacity); 87 | }) 88 | 89 | </script> 90 | 91 | <template> 92 | <teleport :to="isFullscreen ? parent : 'body'"> 93 | <div 94 | ref="tooltip" 95 | role="tooltip" 96 | :aria-hidden="!show" 97 | aria-live="polite" 98 | data-cy="tooltip" 99 | :class="{'vue-data-ui-custom-tooltip' : isCustom, 'vue-data-ui-tooltip': !isCustom}" 100 | v-if="show" 101 | :style="`pointer-events:none;top:${position.top}px;left:${position.left}px;${isCustom ? '' : `background:${convertedBackground};color:${color};max-width:${maxWidth};font-size:${fontSize}px`};border-radius:${borderRadius}px;border:${borderWidth}px solid ${borderColor};z-index:2147483647;`" 102 | > 103 | <slot name="tooltip-before"/> 104 | <slot/> 105 | <div v-html="content"/> 106 | <slot name="tooltip-after"/> 107 | </div> 108 | </teleport> 109 | </template> 110 | 111 | <style> 112 | .vue-data-ui-tooltip { 113 | box-shadow: 0 6px 12px -6px rgba(0,0,0,0.2); 114 | position: fixed; 115 | padding:12px; 116 | backdrop-filter: blur(10px); 117 | -webkit-backdrop-filter: blur(10px); 118 | } 119 | .vue-data-ui-custom-tooltip { 120 | position: fixed; 121 | z-index: 3; 122 | } 123 | </style> -------------------------------------------------------------------------------- /src/atoms/vue-ui-pattern.vue: -------------------------------------------------------------------------------- 1 | <script setup> 2 | import { computed } from "vue"; 3 | import usePatterns from '../usePatterns'; 4 | 5 | const props = defineProps({ 6 | name: { 7 | type: String, 8 | required: true 9 | }, 10 | id: { 11 | type: String, 12 | required: true 13 | }, 14 | fill: { 15 | type: String, 16 | default: '#FFFFFF00' 17 | }, 18 | stroke: { 19 | type: String, 20 | default: '#2D353C' 21 | }, 22 | strokeWidth: { 23 | type: Number, 24 | default: 1 25 | }, 26 | scale: { 27 | type: Number, 28 | default: 1 29 | }, 30 | }); 31 | 32 | const patterns = usePatterns(); 33 | const pattern = computed(() => patterns[props.name]); 34 | 35 | </script> 36 | 37 | <template> 38 | <pattern 39 | :id="id" 40 | :height="pattern.height" 41 | :width="pattern.width" 42 | :patternTransform="`scale(${props.scale})`" 43 | patternUnits="userSpaceOnUse" 44 | > 45 | <rect width="100%" height="100%" :fill="fill"/> 46 | <path 47 | :fill="pattern.hasFill ? props.stroke : 'none'" 48 | :stroke="pattern.hasFill ? 'none' : props.stroke" 49 | :stroke-width="props.strokeWidth" 50 | :stroke-linecap="pattern.strokeLinecap" 51 | :d="pattern.path" 52 | /> 53 | </pattern> 54 | </template> 55 | 56 | -------------------------------------------------------------------------------- /src/calcTooltipPosition.js: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | export function calcTooltipPosition({ tooltip, chart, clientPosition, positionPreference = 'center', defaultOffsetY = 24, blockShiftY = false}) { 4 | const offsetX = ref(0); 5 | const offsetY = ref(defaultOffsetY); 6 | if (tooltip && chart) { 7 | const { width, height } = tooltip.getBoundingClientRect(); 8 | const { right, left, bottom } = chart.getBoundingClientRect(); 9 | 10 | if (positionPreference === 'center') { 11 | if (clientPosition.x + width / 2 > right) { 12 | offsetX.value = -width + (right - clientPosition.x) 13 | } else if (clientPosition.x - width / 2 < left) { 14 | offsetX.value = -width + (width - (clientPosition.x - left)) 15 | } else { 16 | offsetX.value = -width / 2; 17 | } 18 | } 19 | 20 | if (positionPreference === 'right') { 21 | if (clientPosition.x + width > right) { 22 | offsetX.value = -width + (right - clientPosition.x) 23 | } else { 24 | offsetX.value = 0; 25 | } 26 | } 27 | 28 | if (positionPreference === 'left') { 29 | if (clientPosition.x < left + width) { 30 | offsetX.value = -width + (width - (clientPosition.x - left)) 31 | } else { 32 | offsetX.value = -width; 33 | } 34 | } 35 | 36 | if (clientPosition.y + height > bottom && !blockShiftY) { 37 | offsetY.value = -height - defaultOffsetY; 38 | } 39 | } 40 | return { 41 | top: clientPosition.y + offsetY.value, 42 | left: clientPosition.x + offsetX.value 43 | } 44 | } -------------------------------------------------------------------------------- /src/components/vue-data-ui.cy.js: -------------------------------------------------------------------------------- 1 | import VueDataUi from './vue-data-ui.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | 4 | describe('VueDataUi', () => { 5 | 6 | it('handles invalid component gracefully', () => { 7 | cy.mount(VueDataUi, { props: { component: 'InvalidComponent' } }); 8 | cy.contains('The provided component InvalidComponent does not exist.') 9 | }); 10 | 11 | components.forEach(({ name: component, dataset, wrapperClass, config={} }) => { 12 | it(`renders ${component} inside VueDataUi`, () => { 13 | cy.mount(VueDataUi, { 14 | props: { component, dataset, config }, 15 | }).then(() => { 16 | cy.get(wrapperClass).should('be.visible'); 17 | }); 18 | }); 19 | }); 20 | 21 | // TODO: test emits for each component 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/vue-ui-3d-bar.cy.js: -------------------------------------------------------------------------------- 1 | import VueUi3dBar from "./vue-ui-3d-bar.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | describe('<VueUi3dBar />', () => { 6 | it('renders with simple dataset', () => { 7 | cy.viewport(260, 500) 8 | cy.mount(VueUi3dBar, { 9 | props: { 10 | dataset: components.find(c => c.name === 'VueUi3dBar').dataset 11 | } 12 | }).then(() => { 13 | testCommonFeatures({ userOptions: true }) 14 | cy.log('Data label'); 15 | cy.get('[data-cy="vue-ui-3d-bar-simple-datalabel"]').should('exist').and('contain', '100%') 16 | }); 17 | }); 18 | 19 | it('renders with breakdown dataset', () => { 20 | const dataset = components.find(c => c.name === 'VueUi3dBar').dataset2; 21 | cy.viewport(500, 500) 22 | cy.mount(VueUi3dBar, { 23 | props: { 24 | dataset 25 | } 26 | }).then(({ wrapper }) => { 27 | testCommonFeatures({ userOptions: true }); 28 | 29 | cy.get('.vue-ui-3d-bar-stack').should('exist').and('have.length', 6) 30 | dataset.series.forEach((ds) => { 31 | cy.get(`[data-cy="bar-3d-value-${ds.value}"]`).should('exist').and('be.visible') 32 | }) 33 | 34 | cy.log('Props reactivity') 35 | wrapper.setProps({ 36 | dataset: { 37 | series: [dataset.series[0], dataset.series[1]] 38 | } 39 | }).then(() => { 40 | cy.get('.vue-ui-3d-bar-stack').should('exist').and('have.length', 2) 41 | }); 42 | }); 43 | }); 44 | 45 | it('emits', () => { 46 | const dataset = components.find(c => c.name === 'VueUi3dBar').dataset2; 47 | cy.mount(VueUi3dBar, { 48 | props: { 49 | dataset 50 | } 51 | }).then(({ wrapper }) => { 52 | cy.get('.vue-ui-3d-bar-stack').eq(0).find('path').eq(0).click().then(() => { 53 | cy.wrap(wrapper.vm.$nextTick()).then(() => { 54 | expect(wrapper.emitted('selectDatapoint')).to.exist; 55 | expect(wrapper.emitted('selectDatapoint').length).to.equal(2); 56 | }); 57 | }); 58 | }); 59 | }); 60 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-age-pyramid.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiAgePyramid from "./vue-ui-age-pyramid.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiAgePyramid'); 6 | 7 | describe('<VueUiAgePyramid />', () => { 8 | it('renders', () => { 9 | cy.viewport(500, 540) 10 | cy.mount(VueUiAgePyramid, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | 17 | testCommonFeatures({ 18 | userOptions: true, 19 | title: true, 20 | subtitle: true, 21 | dataTable: true, 22 | tooltipCallback: () => { 23 | cy.get('[data-cy="tooltip-trap"]').eq(0).trigger('mouseover'); 24 | } 25 | }); 26 | 27 | cy.log('labels') 28 | cy.get('[data-cy="label-left"]').should('exist').and('be.visible').and('contain', 'female'); 29 | cy.get('[data-cy="label-right"]').should('exist').and('be.visible').and('contain', 'male'); 30 | cy.get('[data-cy="y-axis-label"]').as('y').should('exist').and('have.length', 2) 31 | cy.get('@y').first().contains('5') 32 | cy.get('@y').last().contains('0') 33 | 34 | cy.log('scale') 35 | cy.get('[data-cy="scale-line-left"]').should('exist') 36 | cy.get('[data-cy="scale-line-right"]').should('exist') 37 | cy.get('[data-cy="scale-tick-left"]').should('exist').and('have.length', 6) 38 | cy.get('[data-cy="scale-tick-right"]').should('exist').and('have.length', 6) 39 | cy.get('[data-cy="scale-tick-left-label"]').as('labelsLeft').should('exist').and('have.length', 6) 40 | cy.get('[data-cy="scale-tick-right-label"]').as('labelsRight').should('exist').and('have.length', 6) 41 | cy.get('@labelsLeft').first().should('contain', 400) 42 | cy.get('@labelsLeft').last().should('contain', 0) 43 | cy.get('@labelsRight').first().should('contain', 0) 44 | cy.get('@labelsRight').last().should('contain', 400) 45 | }); 46 | }); 47 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-annotator.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiAnnotator from './vue-ui-annotator.vue'; 2 | 3 | describe('<VueUiAnnotator />', () => { 4 | beforeEach(function () { 5 | cy.viewport(1000, 1100); 6 | }); 7 | 8 | 9 | it('renders with different config attributes', function () { 10 | cy.mount(VueUiAnnotator, { 11 | slots: { 12 | default: `<div style="margin: 0 auto;width:900px;height:900px;border:1px solid blue"></div>` 13 | } 14 | }); 15 | 16 | cy.get(`[data-cy="annotator-summary"]`).click(); 17 | 18 | cy.get(`[data-cy="annotator-button-circle"]`).click(); 19 | 20 | function undo(times = 1) { 21 | for (let i = 0; i < times; i += 1) { 22 | cy.get(`[data-cy="annotator-button-undo"]`).click(); 23 | } 24 | } 25 | 26 | cy.get('#annotatorSvg').then(($svg) => { 27 | cy.wrap($svg) 28 | .trigger('pointermove', { clientX: 500, clientY: 500 }) 29 | .trigger('pointerdown') 30 | .trigger('pointermove', { clientX: 600, clientY: 600 }) 31 | .wait(200) 32 | .trigger('pointerup', { force: true }) 33 | .find('circle') 34 | .should('have.length', 2); 35 | 36 | cy.get(`[data-cy="annotator-button-rect"]`).click(); 37 | 38 | cy.wrap($svg) 39 | .trigger('pointermove', { clientX: 450, clientY: 450 }) 40 | .trigger('pointerdown') 41 | .trigger('pointermove', { clientX: 550, clientY: 550 }) 42 | .wait(200) 43 | .trigger('pointerup', { force: true }) 44 | .find('rect') 45 | .should('have.length', 4); 46 | 47 | cy.get(`[data-cy="annotator-button-arrow"]`).click(); 48 | 49 | cy.wrap($svg) 50 | .trigger('pointermove', { clientX: 400, clientY: 700 }) 51 | .trigger('pointerdown') 52 | .trigger('pointermove', { clientX: 600, clientY: 700 }) 53 | .wait(200) 54 | .trigger('pointerup', { force: true }) 55 | .find('path') 56 | .should('have.length', 1) 57 | 58 | undo(); 59 | 60 | cy.get(`[data-cy="annotator-button-freehand"]`).click(); 61 | cy.wrap($svg) 62 | .trigger('pointermove', { clientX: 400, clientY: 700 }) 63 | .trigger('pointerdown') 64 | .trigger('pointermove', { clientX: 420, clientY: 680 }) 65 | .wait(10) 66 | .trigger('pointermove', { clientX: 440, clientY: 720 }) 67 | .trigger('pointermove', { clientX: 460, clientY: 680 }) 68 | .trigger('pointermove', { clientX: 480, clientY: 720 }) 69 | .trigger('pointermove', { clientX: 500, clientY: 680 }) 70 | .trigger('pointermove', { clientX: 520, clientY: 720 }) 71 | .trigger('pointermove', { clientX: 540, clientY: 680 }) 72 | .trigger('pointermove', { clientX: 560, clientY: 720 }) 73 | .trigger('pointermove', { clientX: 580, clientY: 680 }) 74 | .trigger('pointermove', { clientX: 600, clientY: 720 }) 75 | .trigger('pointermove', { clientX: 620, clientY: 680 }) 76 | .trigger('pointerup', { force: true }) 77 | .find('path') 78 | .should('have.length', 1); 79 | 80 | undo(2); 81 | cy.get(`[data-cy="annotator-button-move"]`).click(); 82 | 83 | cy.wrap($svg) 84 | .trigger('pointermove', { clientX: 500, clientY: 500 }) 85 | .trigger('pointerdown') 86 | .trigger('pointermove', { clientX: 600, clientY: 600 }) 87 | .wait(200) 88 | .trigger('pointerup', { force: true }) 89 | .find('circle') 90 | .should('have.length', 2); 91 | 92 | undo(); 93 | 94 | cy.get(`[data-cy="annotator-button-text"]`).click(); 95 | 96 | cy.wrap($svg) 97 | .trigger('pointermove', { clientX: 400, clientY: 700 }) 98 | .click() 99 | .type('HELLO WORLD') 100 | .find('text') 101 | .should('exist') 102 | .contains('HELLO WORLD') 103 | undo(); 104 | }) 105 | }) 106 | }) -------------------------------------------------------------------------------- /src/components/vue-ui-bullet.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiBullet from "./vue-ui-bullet.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiBullet'); 6 | 7 | describe('<VueUiBullet />', () => { 8 | it('renders', () => { 9 | cy.mount(VueUiBullet, { 10 | props: { 11 | dataset, 12 | config 13 | } 14 | }).then(() => { 15 | testCommonFeatures({ 16 | userOptions: true, 17 | title: true, 18 | subtitle: true, 19 | legend: true 20 | }); 21 | 22 | cy.log('Legend items'); 23 | cy.get('.vue-data-ui-legend-item').as('legend').should('have.length', 3); 24 | cy.get('@legend').each((item, i) => { 25 | cy.wrap(item).as('item'); 26 | cy.get('@item').contains(dataset.segments[i].name); 27 | cy.get('@item').contains(dataset.segments[i].from); 28 | cy.get('@item').contains(dataset.segments[i].to); 29 | }); 30 | 31 | cy.log('Bullet segments'); 32 | cy.get('[data-cy="vue-ui-bullet-segment"]').should('exist').and('be.visible').and('have.length', 3); 33 | 34 | cy.log('Bullet target'); 35 | cy.get('[data-cy="vue-ui-bullet-target-top"]').should('exist').and('be.visible'); 36 | 37 | cy.log('Value label'); 38 | cy.get('[data-cy="vue-ui-bullet-value-label"]').should('exist').and('be.visible').and('contain', dataset.value); 39 | 40 | cy.log('Value bar'); 41 | cy.get('[data-cy="vue-ui-bullet-value-bar"]').should('exist').and('be.visible'); 42 | 43 | cy.log('Ticks'); 44 | cy.get('[data-cy="vue-ui-bullet-tick-label"]').as('tickLabels').should('exist').and('be.visible').and('have.length', 11); 45 | cy.get('@tickLabels').first().contains(dataset.segments[0].from); 46 | cy.get('@tickLabels').last().contains(dataset.segments.at(-1).to); 47 | cy.get('[data-cy="vue-ui-bullet-tick-marker"]').should('exist').and('have.length', 11); 48 | }); 49 | }); 50 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-candlestick.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiCandlestick from './vue-ui-candlestick.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiCandlestick'); 6 | 7 | describe('<VueUiCandlestick />', () => { 8 | 9 | it('renders', () => { 10 | cy.mount(VueUiCandlestick, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | testCommonFeatures({ 17 | userOptions: true, 18 | title: true, 19 | subtitle: true, 20 | slicer: true, 21 | dataTable: true, 22 | tooltipCallback: () => { 23 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseover'); 24 | } 25 | }); 26 | 27 | cy.log('Grid axes'); 28 | cy.get('[data-cy="candlestick-grid-y-axis"]').should('exist').and('be.visible'); 29 | cy.get('[data-cy="candlestick-grid-x-axis"]').should('exist').and('be.visible'); 30 | 31 | cy.log('Y scale ticks'); 32 | cy.get('[data-cy="y-scale-tick"]').should('exist').and('have.length', 9); 33 | 34 | cy.log('Y scale labels'); 35 | cy.get('[data-cy="y-scale-label"]').as('scaleLabels').should('exist').and('have.length', 9); 36 | cy.get('@scaleLabels').first().contains('0'); 37 | cy.get('@scaleLabels').last().contains('160'); 38 | 39 | cy.log('X axis labels'); 40 | cy.get('[data-cy="x-label"]').as('xLabels').should('exist').and('be.visible').and('have.length', 5); 41 | cy.get('@xLabels').first().contains(dataset[0][0]); 42 | cy.get('@xLabels').last().contains(dataset.at(-1)[0]); 43 | }); 44 | }); 45 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-carousel-table.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiCarouselTable from "./vue-ui-carousel-table.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiCarouselTable'); 6 | 7 | describe('<VueUiCarouselTable />', () => { 8 | it('renders', () => { 9 | cy.spy(window, 'requestAnimationFrame').as('rafSpy'); 10 | cy.mount(VueUiCarouselTable, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | cy.log('Animating'); 17 | cy.get('@rafSpy').should('have.been.called'); 18 | 19 | testCommonFeatures({ 20 | userOptions: true, 21 | }); 22 | 23 | cy.log('Caption'); 24 | cy.get('[data-cy="caption"]').should('exist').and('be.visible').and('contain', config.caption.text); 25 | 26 | cy.log('Th'); 27 | cy.get('th').should('have.length', 7); 28 | cy.get('th').each((th, i) => { 29 | cy.wrap(th).as('th') 30 | cy.get('@th').contains(dataset.head[i]); 31 | }); 32 | 33 | cy.log('Pause'); 34 | cy.get('[data-cy="user-options-summary"]').click(); 35 | cy.get('[data-cy="user-options-anim"]').click(); 36 | cy.wait(100); 37 | cy.get('@rafSpy').then(rafSpy => { 38 | const callCountBefore = rafSpy.callCount; 39 | cy.wait(100); 40 | cy.get('@rafSpy').should(spy => { 41 | expect(spy.callCount).to.eq(callCountBefore); 42 | }); 43 | }); 44 | 45 | cy.log('restart'); 46 | cy.get('[data-cy="user-options-anim"]').click(); 47 | cy.wait(100); 48 | cy.get('@rafSpy').should(spy => { 49 | expect(spy).to.have.been.calledWith(Cypress.sinon.match.func); 50 | }); 51 | }) 52 | }) 53 | }) -------------------------------------------------------------------------------- /src/components/vue-ui-circle-pack.cy.js: -------------------------------------------------------------------------------- 1 | import vueUiCirclePack from "./vue-ui-circle-pack.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | import VueUiCirclePack from "./vue-ui-circle-pack.vue"; 5 | 6 | const { dataset, config } = components.find(c => c.name === 'VueUiCirclePack'); 7 | 8 | describe('<VueUiCirclePack />', () => { 9 | 10 | it('renders', () => { 11 | cy.mount(VueUiCirclePack, { 12 | props: { 13 | dataset, 14 | config 15 | } 16 | }).then(() => { 17 | testCommonFeatures({ 18 | userOptions: true, 19 | title: true, 20 | subtitle: true, 21 | dataTable: true, 22 | }); 23 | 24 | cy.log('datapoints'); 25 | cy.get('[data-cy="datapoint-circle"]').should('exist').and('be.visible').and('have.length', dataset.length); 26 | 27 | cy.log('name labels'); 28 | cy.get('[data-cy="label-name"]').as('nameLabels').should('exist').and('be.visible').and('have.length', dataset.length); 29 | cy.get('@nameLabels').each((label, _i) => { 30 | cy.wrap(label).as('label'); 31 | cy.get('@label').contains('d_'); 32 | }); 33 | 34 | cy.log('value labels'); 35 | cy.get('[data-cy="label-value"]').as('nameLabels').should('exist').and('be.visible').and('have.length', dataset.length); 36 | cy.get('@nameLabels').each((label, _i) => { 37 | cy.wrap(label).as('label'); 38 | cy.get('@label').invoke('text').then(text => { 39 | const num = parseInt(text.trim(), 10); 40 | expect(num).to.be.a('number'); 41 | }); 42 | }); 43 | 44 | cy.log('zoom'); 45 | cy.get('[data-cy="datapoint-circle"]').first().trigger('mouseenter', { force: true }); 46 | cy.get('[data-cy="datapoint-zoom"]').as('zoom').should('exist').and('be.visible'); 47 | cy.get('[data-cy="datapoint-zoom-label-name"]').as('zoomName').should('exist').and('be.visible').and('contain', 'd_'); 48 | cy.get('[data-cy="datapoint-zoom-label-value"]').as('zoomValue').should('exist').and('be.visible').invoke('text').then(text => { 49 | const num = parseInt(text.trim(), 10); 50 | expect(num).to.be.a('number'); 51 | }); 52 | 53 | cy.log('close zoom'); 54 | cy.get('[data-cy="datapoint-circle"]').first().trigger('mouseout'); 55 | cy.get('@zoom').should('not.exist'); 56 | cy.get('@zoomName').should('not.exist'); 57 | cy.get('@zoomValue').should('not.exist'); 58 | }); 59 | }); 60 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-cursor.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiCursor from "./vue-ui-cursor.vue"; 2 | 3 | describe('<VueUiCursor />', () => { 4 | 5 | beforeEach(() => { 6 | cy.get('[data-cy-root]').then($root => { 7 | const div = document.createElement('div'); 8 | div.id = 'container'; 9 | div.style.position="relative"; 10 | div.style.height = '400px'; 11 | div.style.width = '400px'; 12 | div.style.background = '#1A1A1A'; 13 | div.innerText = 'HW!'; 14 | $root.append(div); 15 | }); 16 | }); 17 | 18 | it('renders', () => { 19 | cy.viewport(500,500); 20 | cy.mount(VueUiCursor, { 21 | props: { 22 | parentId: 'container' 23 | } 24 | }).then(() => { 25 | cy.get('[data-cy="vue-ui-cursor"]').should('exist'); 26 | 27 | cy.get('#container').trigger('mousemove', { clientX: 250, clientY: 250 }); 28 | cy.get('[data-cy="center-circle"]').should('exist').and('have.css', 'opacity', '1'); 29 | cy.get('[data-cy="bubble"]').should('exist').and('have.css', 'opacity', '1'); 30 | 31 | cy.get('#container').click(); 32 | cy.get('[data-cy="wave"]').should('exist').and('have.css', 'opacity'); 33 | 34 | cy.log('crosshair'); 35 | cy.get('[data-cy="crosshair-x-left"]').should('exist').and('have.css', 'opacity', '1'); 36 | cy.get('[data-cy="crosshair-x-right"]').should('exist').and('have.css', 'opacity', '1'); 37 | cy.get('[data-cy="crosshair-y-top"]').should('exist').and('have.css', 'opacity', '1'); 38 | cy.get('[data-cy="crosshair-y-bottom"]').should('exist').and('have.css', 'opacity', '1'); 39 | 40 | cy.log('intersect circles'); 41 | cy.get('[data-cy="intersect-circle-left"]').should('exist').and('have.css', 'opacity', '1'); 42 | cy.get('[data-cy="intersect-circle-top"]').should('exist').and('have.css', 'opacity', '1'); 43 | cy.get('[data-cy="intersect-circle-right"]').should('exist').and('have.css', 'opacity', '1'); 44 | cy.get('[data-cy="intersect-circle-bottom"]').should('exist').and('have.css', 'opacity', '1'); 45 | 46 | cy.log('coordinates'); 47 | cy.get('[data-cy="coordinates-x"]').should('exist').and('contain', '158'); 48 | cy.get('[data-cy="coordinates-y"]').should('exist').and('contain', '158'); 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-digits.vue: -------------------------------------------------------------------------------- 1 | <script setup> 2 | import { computed, ref } from "vue"; 3 | import Digit from '../atoms/Digit.vue'; 4 | import { useNestedProp } from "../useNestedProp"; 5 | import { XMLNS } from "../lib"; 6 | import { useConfig } from "../useConfig"; 7 | 8 | const { vue_ui_digits: DEFAULT_CONFIG } = useConfig(); 9 | 10 | const props = defineProps({ 11 | dataset: { 12 | type: Number, 13 | default: 0 14 | }, 15 | config: { 16 | type: Object, 17 | default() { 18 | return {} 19 | } 20 | } 21 | }); 22 | 23 | const FINAL_CONFIG = computed(() => { 24 | return useNestedProp({ 25 | userConfig: props.config, 26 | defaultConfig: DEFAULT_CONFIG 27 | }); 28 | }); 29 | 30 | const digits = computed(() => { 31 | const d = (props.dataset || 0).toString().split(''); 32 | const digits = []; 33 | const init = { 34 | x: 10, 35 | y: 10 36 | } 37 | let digitWidth = 0; 38 | for(let i = 0; i < d.length; i += 1) { 39 | const digit = d[i]; 40 | digits.push({ 41 | x: init.x + digitWidth, 42 | y: init.y, 43 | quanta: digit 44 | }) 45 | if(digit == '.') { 46 | digitWidth += 2; 47 | } else { 48 | digitWidth += 44; 49 | } 50 | } 51 | return digits; 52 | }) 53 | 54 | const maxY = computed(() => { 55 | return Math.max(...digits.value.map(d => d.x)) + 36 56 | }) 57 | 58 | </script> 59 | 60 | <template> 61 | <svg class="vue-ui-digits" :xmlns="XMLNS" :viewBox="`0 0 ${maxY} 80`" :style="`background:${FINAL_CONFIG.backgroundColor};${FINAL_CONFIG.height ? `height:${FINAL_CONFIG.height};` : ''}${FINAL_CONFIG.width ? `width:${FINAL_CONFIG.width}` : ''}`"> 62 | <Digit 63 | v-for="digit in digits" 64 | :x="digit.x" 65 | :y="digit.y" 66 | :quanta="digit.quanta" 67 | :color="FINAL_CONFIG.digits.color" 68 | :backgroundColor="FINAL_CONFIG.digits.skeletonColor" 69 | /> 70 | </svg> 71 | </template> -------------------------------------------------------------------------------- /src/components/vue-ui-donut-evolution.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiDonutEvolution from './vue-ui-donut-evolution.vue' 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiDonutEvolution'); 6 | 7 | describe('<VueUiDonutEvolution />', () => { 8 | 9 | it('renders', () => { 10 | cy.viewport(800,800); 11 | cy.mount(VueUiDonutEvolution, { 12 | props: { 13 | dataset, 14 | config 15 | } 16 | }).then(({ wrapper }) => { 17 | 18 | testCommonFeatures({ 19 | userOptions: true, 20 | title: true, 21 | subtitle: true, 22 | dataTable: true, 23 | slicer: true 24 | }); 25 | 26 | cy.log('grid'); 27 | cy.get('[data-cy="axis-y"]').should('exist').and('have.css', 'opacity', '1'); 28 | cy.get('[data-cy="axis-x"]').should('exist').and('have.css', 'opacity', '1'); 29 | cy.get('[data-cy="vertical-separator"]').should('exist').and('have.css', 'opacity', '1').and('have.length', Math.max(...dataset.map(d => d.values.length))); 30 | 31 | cy.log('y axis labels'); 32 | cy.get('[data-cy="axis-y-tick"]').should('exist').and('have.css', 'opacity', '1').and('have.length', 7); 33 | cy.get('[data-cy="axis-y-label"]').as('yLabels').should('exist').and('be.visible').and('have.length', 7); 34 | cy.get('@yLabels').first().contains(0); 35 | cy.get('@yLabels').last().contains(300); 36 | 37 | cy.log('x axis labels'); 38 | cy.get('[data-cy="axis-x-label"]').as('xLabels').should('exist').and('be.visible').and('have.length', config.style.chart.layout.grid.xAxis.dataLabels.values.length); 39 | cy.get('@xLabels').first().contains(config.style.chart.layout.grid.xAxis.dataLabels.values[0]); 40 | cy.get('@xLabels').last().contains(config.style.chart.layout.grid.xAxis.dataLabels.values.at(-1)); 41 | 42 | cy.log('shows zoomed donut on trap click'); 43 | cy.get('[data-cy-trap]').eq(0).click(); 44 | cy.get('[data-cy-zoom]').should('be.visible'); 45 | cy.get('[data-cy-zoom-donut]').should('be.visible'); 46 | cy.get('[data-cy-close]').should('be.visible').click({ force: true}); 47 | cy.get('[data-cy-zoom]').should('not.exist'); 48 | 49 | cy.log('segregates series when selecting legend items'); 50 | cy.get('[data-cy="legend-item"]').eq(0).click().then(() => { 51 | cy.get(`[data-cy="arc_0"]`).should('have.length', 3); 52 | expect(wrapper.emitted('selectLegend')).to.exist; 53 | cy.get('[data-cy="legend-item"]').eq(0).click(); 54 | cy.get(`[data-cy="arc_0"]`).should('have.length', 4); 55 | }) 56 | 57 | cy.log('shows donut hovered state'); 58 | cy.get('[data-cy-trap]').eq(0).trigger('mouseenter'); 59 | cy.get('[data-cy="donut_hover_0"]').should('have.length', 3); 60 | cy.get('[data-cy-trap]').eq(0).trigger('mouseleave'); 61 | cy.get('[data-cy="donut_hover_0"]').should('not.exist'); 62 | }); 63 | }); 64 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-dumbbell.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiDumbbell from "./vue-ui-dumbbell.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiDumbbell'); 6 | 7 | describe('<VueUiDumbbell />', () => { 8 | it('renders', () => { 9 | cy.spy(window, 'requestAnimationFrame').as('rafSpy'); 10 | 11 | cy.mount(VueUiDumbbell, { 12 | props: { 13 | dataset, 14 | config 15 | } 16 | }).then(() => { 17 | cy.log('Animating'); 18 | cy.get('@rafSpy').should('have.been.called'); 19 | 20 | testCommonFeatures({ 21 | userOptions: true, 22 | title: true, 23 | subtitle: true, 24 | dataTable: true, 25 | }); 26 | 27 | cy.log('grid'); 28 | cy.get('[data-cy="grid-line-y"]').should('exist').and('have.length', 9).and('have.css', 'opacity', '1'); 29 | cy.get('[data-cy="grid-line-x"]').should('exist').and('have.length', 6).and('have.css', 'opacity', '1'); 30 | cy.get('[data-cy="grid-base-x"]').should('exist').and('have.css', 'opacity', '1'); 31 | 32 | cy.log('y labels'); 33 | cy.get('[data-cy="label-y-name"]').as('yLabelNames').should('exist').and('be.visible'); 34 | cy.get('@yLabelNames').each((label, i) => { 35 | cy.wrap(label).as('label'); 36 | cy.get('@label').contains(dataset[i].name); 37 | }); 38 | cy.get('[data-cy="label-y-value"]').should('exist').and('be.visible').and('contain', '%'); 39 | 40 | cy.log('x labels'); 41 | cy.get('[data-cy="label-x"]').as('xLabels').should('exist').and('be.visible').and('have.length', 9); 42 | 43 | cy.log('links'); 44 | cy.get('[data-cy="link-curved"]').should('exist').and('be.visible').and('have.length', dataset.length); 45 | 46 | cy.log('datapoints'); 47 | cy.get('[data-cy="datapoint-start"]').should('exist').and('be.visible').and('have.length', dataset.length); 48 | cy.get('[data-cy="datapoint-end"]').should('exist').and('be.visible').and('have.length', dataset.length); 49 | cy.get('[data-cy="datapoint-label-start"]').should('exist').and('be.visible').and('have.length', dataset.length); 50 | cy.get('[data-cy="datapoint-label-start"]').should('exist').and('be.visible').and('have.length', dataset.length); 51 | cy.get('[data-cy="datapoint-label-end"]').should('exist').and('be.visible').and('have.length', dataset.length); 52 | cy.get('[data-cy="datapoint-label-end"]').should('exist').and('be.visible').and('have.length', dataset.length); 53 | }); 54 | }); 55 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-flow.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiFlow from "./vue-ui-flow.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiFlow'); 6 | 7 | describe('<VueUiFlow />', () => { 8 | 9 | it('renders', () => { 10 | cy.mount(VueUiFlow, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | 17 | testCommonFeatures({ 18 | userOptions: true, 19 | title: true, 20 | subtitle: true, 21 | dataTable: true 22 | }); 23 | 24 | cy.log('nodes'); 25 | cy.get('[data-cy="node"]').should('exist').and('be.visible').and('have.length', 11); 26 | 27 | cy.log('links'); 28 | cy.get('[data-cy="link"]').should('exist').and('be.visible').and('have.length', 13); 29 | 30 | cy.log('node names'); 31 | cy.get('[data-cy="node-name"]').should('exist').and('have.length', 11); 32 | 33 | cy.log('node values'); 34 | cy.get('[data-cy="node-value"]').should('exist').and('have.length', 11); 35 | }); 36 | }); 37 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-funnel.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiFunnel from "./vue-ui-funnel.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiFunnel'); 6 | 7 | describe('<VueUiFunnel />', () => { 8 | 9 | it('renders', () => { 10 | cy.mount(VueUiFunnel, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | 17 | testCommonFeatures({ 18 | userOptions: true, 19 | title: true, 20 | subtitle: true, 21 | dataTable: true 22 | }); 23 | 24 | cy.log('circle links'); 25 | cy.get('[data-cy="circle-links"]').should('exist').and('have.css', 'opacity', '1'); 26 | 27 | cy.log('datapoints'); 28 | cy.get('[data-cy="datapoint-circle"]').should('exist').and('be.visible').and('have.length', dataset.length); 29 | cy.get('[data-cy="datapoint-label"]').should('exist').and('be.visible').and('have.length', dataset.length).contains('%'); 30 | 31 | cy.log('funnel area'); 32 | cy.get('[data-cy="funnel-area"]').should('exist').and('be.visible'); 33 | 34 | cy.log('datapoint bars'); 35 | cy.get('[data-cy="datapoint-bar"]').should('exist').and('be.visible').and('have.length', dataset.length); 36 | 37 | cy.log('bar names'); 38 | cy.get('[data-cy="bar-name"]').as('names').should('exist').and('be.visible').and('have.length', dataset.length); 39 | cy.get('@names').each((name, i) => { 40 | cy.wrap(name).as('name'); 41 | cy.get('@name').contains(dataset[i].name); 42 | }); 43 | 44 | cy.log('bar values'); 45 | cy.get('[data-cy="bar-value"]').as('values').should('exist').and('be.visible').and('have.length', dataset.length); 46 | }); 47 | }); 48 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-galaxy.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiGalaxy from "./vue-ui-galaxy.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiGalaxy'); 6 | 7 | describe('<VueUiGalaxy />', () => { 8 | 9 | it('renders', () => { 10 | cy.mount(VueUiGalaxy, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | 17 | testCommonFeatures({ 18 | userOptions: true, 19 | title: true, 20 | subtitle: true, 21 | legend: true, 22 | dataTable: true, 23 | tooltipCallback: () => { 24 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseenter', { force: true }) 25 | } 26 | }); 27 | 28 | cy.log('datapoint borders'); 29 | cy.get('[data-cy="datapoint-border"]').should('exist').and('be.visible').and('have.length', dataset.length); 30 | 31 | cy.log('datapoint paths'); 32 | cy.get('[data-cy="datapoint-path"]').should('exist').and('be.visible').and('have.length', dataset.length); 33 | }); 34 | }); 35 | 36 | it('emits', () => { 37 | cy.mount(VueUiGalaxy, { 38 | props: { 39 | config, 40 | dataset 41 | } 42 | }).then(({ wrapper }) => { 43 | cy.log('@selectLegend'); 44 | cy.get('[data-cy="legend-item-0"]').click({ force: true }).then(() => { 45 | expect(wrapper.emitted('selectLegend')).to.exist; 46 | }) 47 | 48 | cy.log('@selectDatapoint'); 49 | cy.get('[data-cy="tooltip-trap"]').first().click({ force: true }).then(() => { 50 | expect(wrapper.emitted('selectDatapoint')).to.exist; 51 | expect(wrapper.emitted('selectDatapoint')[0][0]).to.have.keys( 52 | 'absoluteValues', 53 | 'color', 54 | 'id', 55 | 'name', 56 | 'path', 57 | 'points', 58 | 'proportion', 59 | 'seriesIndex', 60 | 'value' 61 | ); 62 | expect(wrapper.emitted('selectDatapoint')[0][0].seriesIndex).to.equal(1); 63 | }); 64 | }); 65 | }); 66 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-gauge.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiGauge from './vue-ui-gauge.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiGauge'); 6 | 7 | describe('<VueUiGauge />', () => { 8 | 9 | it('renders', () => { 10 | cy.mount(VueUiGauge, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | testCommonFeatures({ 17 | userOptions: true, 18 | title: true, 19 | subtitle: true 20 | }); 21 | 22 | cy.log('gauge arcs'); 23 | cy.get('[data-cy="gauge-arc"]').should('exist').and('be.visible').and('have.length', dataset.series.length); 24 | 25 | cy.log('indicator'); 26 | cy.get('[data-cy="arc-indicator"]').should('exist').and('be.visible'); 27 | 28 | cy.log('arc labels'); 29 | cy.get('[data-cy="arc-label"]').as('arcLabels').should('exist').and('be.visible').and('have.length', dataset.series.length); 30 | cy.get('@arcLabels').each((label, i) => { 31 | cy.wrap(label).contains(dataset.series[i].name); 32 | }); 33 | 34 | cy.log('segment separators'); 35 | cy.get('[data-cy="segment-separator-first-wrapper"]').should('exist').and('be.visible'); 36 | cy.get('[data-cy="segment-separator-first"]').should('exist').and('be.visible'); 37 | cy.get('[data-cy="segment-separator-wrapper"]').should('exist').and('be.visible').and('have.length', dataset.series.length); 38 | cy.get('[data-cy="segment-separator"]').should('exist').and('be.visible').and('have.length', dataset.series.length); 39 | 40 | cy.log('arc value labels'); 41 | cy.get('[data-cy="arc-label-value"]').as('valueLabels').should('exist').and('be.visible').and('have.length', dataset.series.length); 42 | cy.get('@valueLabels').first().contains(dataset.series[0].from); 43 | cy.get('@valueLabels').last().contains(dataset.series.at(-1).from); 44 | cy.get('[data-cy="arc-label-value-last"]').should('exist').and('be.visible').and('contain', dataset.series.at(-1).to); 45 | 46 | cy.log('pointer'); 47 | cy.get('[data-cy="gauge-pointer"]').should('exist').and('be.visible'); 48 | cy.get('[data-cy="gauge-pointer-circle"]').should('exist').and('be.visible'); 49 | 50 | cy.log('score'); 51 | cy.get('[data-cy="gauge-score"]').should('exist').and('be.visible').and('contain', dataset.value); 52 | }); 53 | }); 54 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-gizmo.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiGizmo from "./vue-ui-gizmo.vue"; 2 | 3 | describe('<VueUiGizmo />', () => { 4 | it('renders as a battery', () => { 5 | cy.viewport(410,170) 6 | cy.mount(VueUiGizmo, { 7 | props: { 8 | dataset: 50, 9 | config: { 10 | size: 400 11 | } 12 | } 13 | }).then(() => { 14 | cy.get('[data-cy="battery-shape"]').should('exist').and('be.visible'); 15 | cy.get('[data-cy="battery-cap"]').should('exist').and('have.css', 'opacity', '0.5'); 16 | cy.get('[data-cy="battery-level"]').should('exist').and('be.visible'); 17 | cy.get('[data-cy="battery-label"]').should('exist').and('be.visible').and('contain', '50%'); 18 | }); 19 | }); 20 | 21 | it('renders as a gauge', () => { 22 | cy.mount(VueUiGizmo, { 23 | props: { 24 | dataset: 50, 25 | config: { 26 | type: 'gauge', 27 | size: 400 28 | } 29 | } 30 | }).then(() => { 31 | cy.get('[data-cy="gauge-gutter"]').should('exist').and('be.visible'); 32 | cy.get('[data-cy="gauge-track"]').should('exist').and('be.visible'); 33 | cy.get('[data-cy="gauge-gradient"]').should('exist').and('be.visible'); 34 | cy.get('[data-cy="gauge-label"]').should('exist').and('be.visible').and('contain', '50%'); 35 | 36 | }); 37 | }); 38 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-heatmap.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiHeatmap from './vue-ui-heatmap.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiHeatmap'); 6 | 7 | describe('<VueUiHeatmap />', () => { 8 | 9 | function commonTest() { 10 | testCommonFeatures({ 11 | userOptions: true, 12 | title: true, 13 | subtitle: true, 14 | dataTable: true, 15 | tooltipCallback: () => { 16 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseover', { force: true }); 17 | cy.get('[data-cy="cell-selected"]').should('exist').and('be.visible'); 18 | } 19 | }); 20 | 21 | cy.log('cells'); 22 | cy.get('[data-cy="cell-underlayer"]').should('exist').and('be.visible').and('have.length', 91); 23 | cy.get('[data-cy="cell"]').should('exist').and('be.visible').and('have.length', 91); 24 | cy.get('[data-cy="cell-label"]').should('exist').and('be.visible').and('have.length', 91); 25 | cy.get('[data-cy="tooltip-trap"]').should('exist').and('be.visible').and('have.length', 91); 26 | 27 | cy.log('y axis labels'); 28 | cy.get('[data-cy="axis-y-label"]').as('yLabels').should('exist').and('be.visible').and('have.length', dataset.length); 29 | cy.get('@yLabels').each((label, i) => { 30 | cy.wrap(label).contains(dataset[i].name); 31 | }); 32 | 33 | cy.log('x axis labels'); 34 | cy.get('[data-cy="axis-x-label"]').as('xLabels').should('exist').and('be.visible').and('have.length', 13); 35 | cy.get('@xLabels').each((label, i) => { 36 | cy.wrap(label).contains(config.style.layout.dataLabels.xAxis.values[i]); 37 | }); 38 | 39 | cy.log('legend'); 40 | cy.get('[data-cy="legend-label-max"]').should('exist').and('be.visible').and('contain', 30); 41 | cy.get('[data-cy="legend-label-min"]').should('exist').and('be.visible').and('contain', 0); 42 | cy.get('[data-cy="legend-pill"]').should('exist').and('be.visible'); 43 | cy.get('[data-cy="legend-indicator-line"]').should('exist').and('have.css', 'opacity', '1'); 44 | cy.get('[data-cy="legend-indicator-triangle"]').should('exist').and('be.visible'); 45 | } 46 | 47 | it('renders with right legend', () => { 48 | cy.mount(VueUiHeatmap, { 49 | props: { 50 | config, 51 | dataset 52 | } 53 | }).then(commonTest); 54 | }); 55 | 56 | it('renders with bottom legend', () => { 57 | cy.mount(VueUiHeatmap, { 58 | props: { 59 | config: { 60 | ...config, 61 | style: { 62 | ...config.style, 63 | legend: { 64 | position: 'bottom' 65 | } 66 | } 67 | }, 68 | dataset 69 | } 70 | }).then(commonTest); 71 | }); 72 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-kpi.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiKpi from "./vue-ui-kpi.vue"; 2 | 3 | describe('<VueUiKpi />', () => { 4 | beforeEach(() => { 5 | cy.spy(window, 'requestAnimationFrame').as('rafSpy'); 6 | }); 7 | 8 | it('renders with slots', () => { 9 | cy.mount(VueUiKpi, { 10 | props: { 11 | dataset: 100 12 | }, 13 | slots: { 14 | title: () => "TITLE SLOT", 15 | value: () => "VALUE SLOT", 16 | ['comment-before']: () => "COMMENT BEFORE SLOT", 17 | ['comment-after']: () => "COMMENT AFTER SLOT" 18 | } 19 | }).then(() => { 20 | cy.log('Animating'); 21 | cy.get('@rafSpy').should('have.been.called'); 22 | 23 | cy.get('.vue-ui-kpi') 24 | .as('container') 25 | .should('contain', 'TITLE SLOT') 26 | .and('contain', 'VALUE SLOT') 27 | .and('contain', 'COMMENT BEFORE SLOT') 28 | .and('contain', 'COMMENT AFTER SLOT'); 29 | 30 | cy.get('@container').should('contain', 0); 31 | cy.get('@container').should('contain', 100); 32 | }); 33 | }); 34 | 35 | it('renders with digits', () => { 36 | cy.mount(VueUiKpi, { 37 | props: { 38 | dataset: 100, 39 | config: { 40 | analogDigits: { show: true } 41 | } 42 | } 43 | }).then(() => { 44 | cy.log('Animating'); 45 | cy.get('@rafSpy').should('have.been.called'); 46 | cy.get('.vue-ui-digits').should('exist').and('be.visible'); 47 | }); 48 | }); 49 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-mini-loader.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiMiniLoader from "./vue-ui-mini-loader.vue"; 2 | 3 | describe('<VueUiMiniLoader />', () => { 4 | it('renders default onion variant', () => { 5 | cy.mount(VueUiMiniLoader, {}).then(() => { 6 | cy.get('[data-cy="variant-onion"]').should('exist').and('be.visible'); 7 | cy.get('[data-cy="variant-onion-gutter"]').should('exist').and('be.visible').and('have.length', 3); 8 | cy.get('[data-cy="variant-onion-track"]').should('exist').and('be.visible').and('have.length', 3); 9 | cy.get('.onion-animated').should('exist').and('be.visible').and('have.length', 3); 10 | }); 11 | }); 12 | 13 | it('renders line variant', () => { 14 | cy.mount(VueUiMiniLoader, { 15 | props: { 16 | config: { type: 'line' } 17 | } 18 | }).then(() => { 19 | cy.get('[data-cy="variant-line"]').should('exist').and('be.visible'); 20 | cy.get('[data-cy="variant-line-gutter"]').should('exist').and('be.visible').and('have.length', 1); 21 | cy.get('[data-cy="variant-line-track"]').should('exist').and('be.visible').and('have.length', 1); 22 | cy.get('.line-animated').should('exist').and('be.visible').and('have.length', 1); 23 | }); 24 | }); 25 | 26 | it('renders bar variant', () => { 27 | cy.mount(VueUiMiniLoader, { 28 | props: { 29 | config: { type: 'bar' } 30 | } 31 | }).then(() => { 32 | cy.get('[data-cy="variant-bar"]').should('exist').and('be.visible'); 33 | cy.get('[data-cy="variant-bar-gutter"]').should('exist').and('be.visible').and('have.length', 1); 34 | cy.get('[data-cy="variant-bar-track"]').should('exist').and('be.visible').and('have.length', 1); 35 | cy.get('.bar-animated').should('exist').and('be.visible').and('have.length', 1); 36 | }); 37 | }); 38 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-molecule.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiMolecule from "./vue-ui-molecule.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiMolecule'); 6 | 7 | describe('<VueUiMolecule />', () => { 8 | it('renders', () => { 9 | cy.viewport(500,550); 10 | cy.mount(VueUiMolecule, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | 17 | testCommonFeatures({ 18 | userOptions: true, 19 | title: true, 20 | subtitle: true, 21 | dataTable: true, 22 | tooltipCallback: () => { 23 | cy.get('[data-cy="recursive-circle"]').first().trigger('mouseover', { force: true }); 24 | } 25 | }); 26 | 27 | cy.get('[data-cy="recursive-circle"]').should('exist').and('be.visible').and('have.length', 76); 28 | cy.get('[data-cy="recursive-link-wrapper"]').should('exist').and('be.visible').and('have.length', 75); 29 | cy.get('[data-cy="recursive-link"]').should('exist').and('be.visible').and('have.length', 75); 30 | cy.get('[data-cy="recursive-label"]').should('exist').and('be.visible').and('have.length', 76); 31 | 32 | cy.log('toggle labels'); 33 | cy.get('[data-cy="user-options-summary"]').click(); 34 | cy.get('[data-cy="user-options-label"]').click(); 35 | cy.get('[data-cy="recursive-label"]').should('not.exist') 36 | cy.get('[data-cy="user-options-label"]').click(); 37 | cy.get('[data-cy="recursive-label"]').should('exist').and('be.visible').and('have.length', 76); 38 | }); 39 | }); 40 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-mood-radar.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiMoodRadar from "./vue-ui-mood-radar.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | import { useConfig } from "../useConfig"; 5 | 6 | const { dataset } = components.find(c => c.name === 'VueUiMoodRadar'); 7 | const { vue_ui_mood_radar: config } = useConfig(); 8 | 9 | describe('<VueUiMoodRadar />', () => { 10 | 11 | it('renders', () => { 12 | cy.viewport(500, 600); 13 | cy.mount(VueUiMoodRadar, { 14 | props: { 15 | dataset, 16 | config: { 17 | ...config, 18 | style: { 19 | ...config.style, 20 | chart: { 21 | ...config.style.chart, 22 | title: { 23 | text: 'Title', 24 | subtitle: { text: 'Subtitle' } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | }).then(() => { 31 | 32 | testCommonFeatures({ 33 | userOptions: true, 34 | title: true, 35 | subtitle: true, 36 | legend: true 37 | }); 38 | 39 | cy.log('grid'); 40 | cy.get('[data-cy="grid-radial"]').should('exist').and('have.css', 'opacity', '1').and('have.length', 5) 41 | cy.get('[data-cy="grid-polygon"]').should('exist').and('have.css', 'opacity', '1'); 42 | 43 | cy.log('icons'); 44 | for(let i = 1; i <= 5; i += 1 ) { 45 | cy.get(`[data-cy="icon-${i}"]`).should('exist').and('be.visible').invoke('attr', 'stroke').should('eq', config.style.chart.layout.smileys.colors[i]); 46 | } 47 | 48 | cy.log('traps'); 49 | for(let i = 1; i <= 5; i += 1 ) { 50 | cy.get(`[data-cy="trap-${i}"]`).invoke('attr', 'fill').should('eq', 'transparent'); 51 | cy.get(`[data-cy="trap-${i}"]`).should('exist').and('be.visible').trigger('mouseenter'); 52 | cy.get(`[data-cy="trap-${i}"]`).invoke('attr', 'fill').should('eq', `${config.style.chart.layout.smileys.colors[i]}33`); 53 | cy.log('selection'); 54 | cy.get('[data-cy="datapoint-selection-line"]').should('exist').and('be.visible'); 55 | cy.get('[data-cy="datapoint-selection-circle"]').should('exist').and('be.visible').and('have.length', 10); 56 | cy.get('[data-cy="label-value"]').should('exist').and('be.visible').contains(dataset[i]); 57 | cy.get('[data-cy="label-percentage"]').should('exist').and('be.visible').contains('%'); 58 | } 59 | }); 60 | }); 61 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-onion.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiOnion from "./vue-ui-onion.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiOnion'); 6 | 7 | describe('<VueUiOnion />', () => { 8 | 9 | it('renders', () => { 10 | cy.viewport(500, 580); 11 | cy.mount(VueUiOnion, { 12 | props: { 13 | dataset, 14 | config 15 | } 16 | }).then(() => { 17 | 18 | testCommonFeatures({ 19 | userOptions: true, 20 | title: true, 21 | subtitle: true, 22 | legend: true, 23 | dataTable: true, 24 | tooltipCallback: () => { 25 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseenter', { force: true }); 26 | } 27 | }); 28 | 29 | cy.log('gutters'); 30 | cy.get('[data-cy="onion-gutter"]').should('exist').and('be.visible').and('have.length', dataset.length); 31 | 32 | cy.log('tracks'); 33 | cy.get('[data-cy="onion-track"]').should('exist').and('be.visible').and('have.length', dataset.length); 34 | 35 | cy.log('gradients'); 36 | cy.get('[data-cy="onion-gradient"]').should('exist').and('be.visible').and('have.length', dataset.length); 37 | 38 | cy.log('data labels'); 39 | cy.get('[data-cy="onion-label"]').as('labels').should('exist').and('be.visible').and('have.length', dataset.length); 40 | cy.get('@labels').each((label,i) => { 41 | cy.wrap(label).as('label'); 42 | cy.get('@label') 43 | .should('contain', dataset[i].name) 44 | .and('contain', dataset[i].percentage) 45 | .and('contain', '%') 46 | }); 47 | }); 48 | }); 49 | 50 | it('emits', () => { 51 | cy.viewport(500, 580); 52 | cy.mount(VueUiOnion, { 53 | props: { 54 | dataset, 55 | config 56 | } 57 | }).then(({ wrapper }) => { 58 | cy.log('@selectLegend'); 59 | cy.get('[data-cy-legend-item]').first().click({ force: true }).then(() => { 60 | expect(wrapper.emitted('selectLegend')).to.exist; 61 | }); 62 | }); 63 | }); 64 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-parallel-coordinate-plot.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiParallelCoordinatePlot from "./vue-ui-parallel-coordinate-plot.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiParallelCoordinatePlot'); 6 | 7 | describe('<VueUiParallelCoordinatePlot />', () => { 8 | 9 | it('renders', () => { 10 | cy.mount(VueUiParallelCoordinatePlot, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | 17 | testCommonFeatures({ 18 | userOptions: true, 19 | title: true, 20 | subtitle: true, 21 | dataTable: true, 22 | legend: true, 23 | tooltipCallback: () => { 24 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseenter', { force: true }) 25 | } 26 | }); 27 | 28 | cy.log('axes'); 29 | cy.get('[data-cy="pcp-axis"]').should('exist').and('have.css', 'opacity', '1').and('have.length', Math.max(...dataset.flatMap(d => d.series.map(s => s.values.length)))); 30 | cy.get('[data-cy="pcp-axis-label"]').as('axisLabels').should('exist').and('be.visible').and('have.length', Math.max(...dataset.flatMap(d => d.series.map(s => s.values.length)))); 31 | cy.get('@axisLabels').each((label, i) => { 32 | cy.wrap(label).contains(`Y-${i+1}`) 33 | }); 34 | 35 | cy.log('scales'); 36 | cy.get('[data-cy="scale-tick"]').should('exist').and('have.css', 'opacity', '1').and('have.length', 37); 37 | cy.get('[data-cy="scale-label"]').should('exist').and('be.visible').and('have.length', 37); 38 | 39 | cy.log('plots'); 40 | cy.get('[data-cy="atom-shape"]').should('exist').and('be.visible').and('have.length', dataset.flatMap(d => d.series.map(s => s.values.length)).reduce((a, b) => a + b, 0) + 2 /* legend shapes */); 41 | 42 | cy.log('plot labels'); 43 | cy.get('[data-cy="plot-label"]').should('exist').and('be.visible').and('have.length', dataset.flatMap(d => d.series.map(s => s.values.length)).reduce((a, b) => a + b, 0)); 44 | 45 | cy.log('datapoint lines'); 46 | cy.get('[data-cy="datapoint-line"]').should('exist').and('be.visible').and('have.length', dataset.flatMap(d => d.series).length); 47 | }); 48 | }); 49 | 50 | it('emits', () => { 51 | cy.mount(VueUiParallelCoordinatePlot, { 52 | props: { 53 | dataset, 54 | config 55 | } 56 | }).then(({ wrapper }) => { 57 | cy.log('@selectLegend'); 58 | cy.get('[data-cy="legend-item"]').first().click({ force: true }).then(() => { 59 | expect(wrapper.emitted('selectLegend')).to.exist; 60 | }); 61 | 62 | cy.log('@selectDatapoint'); 63 | cy.get('.vdui-shape-circle').first().click({ force: true }).then(() => { 64 | expect(wrapper.emitted('selectDatapoint')).to.exist; 65 | expect(wrapper.emitted('selectDatapoint')[0][0]).to.have.keys( 66 | 'axisIndex', 67 | 'comment', 68 | 'datapointIndex', 69 | 'name', 70 | 'seriesIndex', 71 | 'seriesName', 72 | 'value', 73 | 'x', 74 | 'y' 75 | ); 76 | expect(wrapper.emitted('selectDatapoint')[0][0].axisIndex).to.equal(0); 77 | expect(wrapper.emitted('selectDatapoint')[0][0].seriesIndex).to.equal(0); 78 | }); 79 | }); 80 | }); 81 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-radar.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiRadar from './vue-ui-radar.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { config, dataset } = components.find(c => c.name === 'VueUiRadar'); 6 | 7 | describe('<VueUiRadar />', () => { 8 | 9 | it('renders', () => { 10 | cy.mount(VueUiRadar, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | 17 | testCommonFeatures({ 18 | userOptions: true, 19 | title: true, 20 | subtitle: true, 21 | legend: true, 22 | dataTable: true, 23 | tooltipCallback: () => { 24 | cy.get('[data-cy="label-apex"]').first().trigger('mouseenter', { force: true }); 25 | } 26 | }); 27 | 28 | cy.log('radial lines'); 29 | cy.get('[data-cy="radial-line"]').should('exist').and('have.css', 'opacity', '1').and('have.length', dataset.series.length); 30 | 31 | cy.log('grid'); 32 | cy.get('[data-cy="polygon-inner"]').should('exist').and('have.css', 'opacity', '1').and('have.length', config.style.chart.layout.grid.graduations); 33 | cy.get('[data-cy="polygon-outer"]').should('exist').and('have.css', 'opacity', '1'); 34 | 35 | cy.log('apex labels'); 36 | cy.get('[data-cy="label-apex"]').as('apexLabels').should('exist').and('be.visible').and('have.length', dataset.series.length); 37 | cy.get('@apexLabels').each((label, i) => { 38 | cy.wrap(label).contains(dataset.series[i].name); 39 | }); 40 | 41 | cy.log('datapoints'); 42 | cy.get('[data-cy="polygon-datapoint-wrapper"]').should('exist').and('be.visible').and('have.length', dataset.categories.length); 43 | cy.get('[data-cy="polygon-datapoint"]').should('exist').and('be.visible').and('have.length', dataset.categories.length); 44 | cy.get('[data-cy="datapoint-circle"]').should('exist').and('be.visible').and('have.length', dataset.categories.length * dataset.series.length); 45 | }); 46 | }); 47 | 48 | it('emits', () => { 49 | cy.mount(VueUiRadar, { 50 | props: { 51 | dataset, 52 | config 53 | } 54 | }).then(({ wrapper }) => { 55 | cy.log('@selectLegend'); 56 | cy.get('[data-cy="legend-item"]').first().click().then(() => { 57 | expect(wrapper.emitted('selectLegend')).to.exist; 58 | }); 59 | }); 60 | }); 61 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-relation-circle.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiRelationCircle from './vue-ui-relation-circle.vue' 2 | 3 | describe('<VueUiRelationCircle />', () => { 4 | 5 | beforeEach(function () { 6 | cy.fixture('relation-circle.json').as('fixture'); 7 | cy.viewport(500, 500); 8 | }); 9 | 10 | it('renders', () => { 11 | cy.get('@fixture').then((fixture) => { 12 | cy.mount(VueUiRelationCircle, { 13 | props: { 14 | dataset: fixture.dataset, 15 | config: fixture.config 16 | }, 17 | }); 18 | 19 | [ 20 | { 21 | selector: `[data-cy="relation-div-title"]`, 22 | expected: fixture.config.style.title.text 23 | }, 24 | { 25 | selector: `[data-cy="relation-div-subtitle"]`, 26 | expected: fixture.config.style.title.subtitle.text 27 | }, 28 | ].forEach(el => { 29 | cy.get(el.selector) 30 | .should('exist') 31 | .contains(el.expected) 32 | }); 33 | 34 | cy.get(`[data-cy="relation-circle"]`).then(($circle) => { 35 | cy.wrap($circle) 36 | .should('exist'); 37 | 38 | [ 39 | { 40 | attr: 'r', 41 | expected: String(fixture.config.style.size * fixture.config.style.circle.radiusProportion) 42 | }, 43 | { 44 | attr: 'cx', 45 | expected: String(fixture.config.style.size / 2) 46 | }, 47 | { 48 | attr: 'cy', 49 | expected: String(fixture.config.style.size / 2 + fixture.config.style.circle.offsetY) 50 | }, 51 | { 52 | attr: 'stroke', 53 | expected: fixture.config.style.circle.stroke 54 | }, 55 | { 56 | attr: 'stroke-width', 57 | expected: String(fixture.config.style.circle.strokeWidth) 58 | }, 59 | ].forEach(el => { 60 | cy.wrap($circle) 61 | .invoke('attr', el.attr) 62 | .should('eq', el.expected) 63 | }); 64 | }); 65 | 66 | for (let i = 0; i < fixture.dataset.length; i += 1) { 67 | cy.get(`[data-cy="relation-text-${i}"]`).then(($text) => { 68 | cy.wrap($text) 69 | .should('exist') 70 | .contains(fixture.dataset[i].label) 71 | .wait(100) 72 | .click(); 73 | 74 | if (i === fixture.dataset.length - 1) { 75 | cy.wrap($text) 76 | .wait(100) 77 | .click(); 78 | } 79 | }); 80 | } 81 | }); 82 | }) 83 | }) -------------------------------------------------------------------------------- /src/components/vue-ui-rings.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiRings from './vue-ui-rings.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { config, dataset } = components.find(c => c.name === 'VueUiRings'); 6 | 7 | describe('<VueUiRings />', () => { 8 | it('renders', () => { 9 | cy.viewport(500, 600); 10 | cy.mount(VueUiRings, { 11 | props: { 12 | config, 13 | dataset 14 | } 15 | }).then(() => { 16 | testCommonFeatures({ 17 | userOptions: true, 18 | title: true, 19 | subtitle: true, 20 | legend: true, 21 | dataTable: true, 22 | tooltipCallback: () => { 23 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseenter', { force: true }); 24 | } 25 | }); 26 | 27 | cy.log('datapoints'); 28 | cy.get('[data-cy="ring-underlayer"]').should('exist').and('be.visible').and('have.length', dataset.length); 29 | cy.get('[data-cy="ring"]').should('exist').and('be.visible').and('have.length', dataset.length); 30 | }); 31 | }); 32 | 33 | it('emits', () => { 34 | cy.mount(VueUiRings, { 35 | props: { 36 | dataset, 37 | config 38 | } 39 | }).then(({ wrapper }) => { 40 | cy.log('@selectLegend'); 41 | cy.get('[data-cy="legend-item"]').first().click().then(() => { 42 | expect(wrapper.emitted('selectLegend')).to.exist 43 | }); 44 | }); 45 | }); 46 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-scatter.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiScatter from './vue-ui-scatter.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { config, dataset } = components.find(c => c.name === 'VueUiScatter'); 6 | 7 | describe('<VueUiScatter />', () => { 8 | it('renders', () => { 9 | cy.mount(VueUiScatter, { 10 | props: { 11 | dataset, 12 | config 13 | } 14 | }).then(() => { 15 | testCommonFeatures({ 16 | userOptions: true, 17 | title: true, 18 | subtitle: true, 19 | dataTable: true, 20 | legend: true, 21 | tooltipCallback: () => { 22 | cy.get('[data-cy="atom-shape"]').first().trigger('mouseover', { force: true }); 23 | } 24 | }); 25 | 26 | cy.log('marginal bars'); 27 | cy.get('[data-cy="marginal-bar-x"]').should('exist').and('be.visible').and('have.length', 16); 28 | cy.get('[data-cy="marginal-bar-y"]').should('exist').and('be.visible').and('have.length', 19); 29 | cy.get('[data-cy="marginal-line-x-wrapper"]').should('exist').and('be.visible'); 30 | cy.get('[data-cy="marginal-line-x"]').should('exist').and('be.visible'); 31 | cy.get('[data-cy="marginal-line-y-wrapper"]').should('exist').and('be.visible'); 32 | cy.get('[data-cy="marginal-line-y"]').should('exist').and('be.visible'); 33 | 34 | cy.log('selection'); 35 | cy.get('[data-cy="atom-shape"]').first().trigger('mouseover', { force: true }); 36 | cy.get('[data-cy="selector-line-x"]').should('exist').and('have.css', 'opacity', '1'); 37 | cy.get('[data-cy="selector-line-y"]').should('exist').and('have.css', 'opacity', '1'); 38 | cy.get('[data-cy="selector-label-x"]').should('exist').and('be.visible').and('contain', dataset[0].values[0].x); 39 | cy.get('[data-cy="selector-label-y"]').should('exist').and('be.visible').and('contain', dataset[0].values[0].y); 40 | cy.get('[data-cy="selector-circle-marker"]').should('exist').and('be.visible').and('have.length', 2); 41 | cy.get('[data-cy="selector-datapoint-name"]').should('exist').and('be.visible').and('contain', dataset[0].values[0].name); 42 | 43 | cy.log('axis labels'); 44 | cy.get('[data-cy="scatter-x-label-name"]').should('exist').and('be.visible').and('contain', config.style.layout.dataLabels.xAxis.name); 45 | cy.get('[data-cy="scatter-x-min-axis-label"]').should('exist').and('be.visible').and('contain', Math.min(...dataset.flatMap(ds => ds.values.map(d => d.x)))); 46 | cy.get('[data-cy="scatter-x-max-axis-label"]').should('exist').and('be.visible').and('contain', Math.max(...dataset.flatMap(ds => ds.values.map(d => d.x)))); 47 | cy.get('[data-cy="scatter-y-label-name"]').should('exist').and('be.visible').and('contain', config.style.layout.dataLabels.yAxis.name); 48 | cy.get('[data-cy="scatter-y-min-axis-label"]').should('exist').and('be.visible').and('contain', Math.min(...dataset.flatMap(ds => ds.values.map(d => d.y)))); 49 | cy.get('[data-cy="scatter-y-max-axis-label"]').should('exist').and('be.visible').and('contain', Math.max(...dataset.flatMap(ds => ds.values.map(d => d.y)))); 50 | 51 | cy.log('correlation'); 52 | cy.get('[data-cy="correlation-line"]').should('exist').and('have.css', 'opacity', '1').and('have.length', dataset.length); 53 | cy.get('[data-cy="correlation-label"]').should('exist').and('be.visible').and('have.length', dataset.length).and('contain', 1); 54 | }); 55 | }); 56 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-skeleton.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiSkeleton from './vue-ui-skeleton.vue' 2 | 3 | describe('<VueUiSkeleton />', () => { 4 | 5 | function updateConfigInFixture(modifiedConfig) { 6 | cy.get('@fixture').then((fixture) => { 7 | const updatedFixture = { ...fixture, config: modifiedConfig }; 8 | cy.wrap(updatedFixture).as('fixture'); 9 | }); 10 | } 11 | 12 | beforeEach(function () { 13 | cy.fixture('skeleton.json').as('fixture'); 14 | cy.viewport(500, 360); 15 | }); 16 | 17 | it('renders', () => { 18 | cy.get('@fixture').then((fixture) => { 19 | cy.mount(VueUiSkeleton, { 20 | props: { 21 | dataset: fixture.dataset, 22 | config: fixture.config 23 | } 24 | }); 25 | 26 | [ 27 | { 28 | type: 'pyramid', 29 | height: 400 30 | }, 31 | { 32 | type: 'sparkline', 33 | height: 120 34 | }, 35 | { 36 | type: 'candlesticks', 37 | height: 330 38 | }, 39 | { 40 | type: 'heatmap', 41 | height: 150 42 | }, 43 | { 44 | type: 'chestnut', 45 | height: 320 46 | }, 47 | { 48 | type: 'line', 49 | height: 360 50 | }, 51 | { 52 | type: 'bar', 53 | height: 360 54 | }, 55 | { 56 | type: 'donut', 57 | height: 500 58 | }, 59 | { 60 | type: 'onion', 61 | height: 500 62 | }, 63 | { 64 | type: 'gauge', 65 | height: 500 66 | }, 67 | { 68 | type: 'quadrant', 69 | height: 500 70 | }, 71 | { 72 | type: 'radar', 73 | height: 500 74 | }, 75 | { 76 | type: 'waffle', 77 | height: 500 78 | }, 79 | { 80 | type: 'table', 81 | height: 360 82 | }, 83 | { 84 | type: 'rating', 85 | height: 80 86 | }, 87 | { 88 | type: 'verticalBar', 89 | height: 500 90 | }, 91 | ].forEach(t => { 92 | cy.wait(300); 93 | 94 | let modifiedConfig = { 95 | ...fixture.config, 96 | type: t.type 97 | } 98 | 99 | updateConfigInFixture(modifiedConfig); 100 | 101 | cy.mount(VueUiSkeleton, { 102 | props: { 103 | dataset: fixture.dataset, 104 | config: modifiedConfig 105 | } 106 | }); 107 | 108 | cy.viewport(500, t.height); 109 | 110 | cy.get(`[data-cy="skeleton-${t.type}"]`) 111 | .should('exist'); 112 | 113 | }); 114 | }); 115 | }) 116 | }) -------------------------------------------------------------------------------- /src/components/vue-ui-smiley.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiSmiley from './vue-ui-smiley.vue' 2 | 3 | describe('<VueUiSmiley />', () => { 4 | beforeEach(function () { 5 | cy.fixture('smiley.json').as('fixture'); 6 | cy.viewport(800, 160); 7 | }); 8 | 9 | function updateConfigInFixture(modifiedConfig) { 10 | cy.get('@fixture').then((fixture) => { 11 | const updatedFixture = { ...fixture, config: modifiedConfig }; 12 | cy.wrap(updatedFixture).as('fixture'); 13 | }); 14 | } 15 | 16 | it('renders with different config attributes', function () { 17 | cy.get('@fixture').then((fixture) => { 18 | cy.mount(VueUiSmiley, { 19 | props: { 20 | dataset: fixture.dataset, 21 | config: fixture.config 22 | } 23 | }); 24 | 25 | function calculateAverageRating(source) { 26 | if (source === null) return null; 27 | let totalSum = 0; 28 | let totalCount = 0; 29 | 30 | for (const key in source) { 31 | const ratingValue = parseInt(key); 32 | const ratingCount = source[key]; 33 | 34 | totalSum += ratingValue * ratingCount; 35 | totalCount += ratingCount; 36 | } 37 | 38 | if (totalCount === 0) { 39 | return 0; 40 | } 41 | 42 | const averageRating = totalSum / totalCount; 43 | return averageRating; 44 | } 45 | 46 | const staticRating = Math.round(calculateAverageRating(fixture.dataset.rating)) 47 | 48 | cy.get(`[data-cy="smiley-title"]`) 49 | .should('exist') 50 | .contains('Title'); 51 | 52 | cy.get(`[data-cy="smiley-subtitle"]`) 53 | .should('exist') 54 | .contains('Subtitle'); 55 | 56 | cy.get(`[data-cy="smiley-position-bottom"]`) 57 | .should('exist') 58 | .contains(staticRating); 59 | 60 | for (let i = 0; i < 5; i += 1) { 61 | cy.get(`[data-cy="smiley-item-${i}"]`) 62 | .should('exist') 63 | .click(); 64 | 65 | cy.get(`[data-cy="smiley-position-bottom"]`) 66 | .contains(`${i + 1}`) 67 | } 68 | 69 | let modifiedConfig = { 70 | ...fixture.config, 71 | readonly: true 72 | } 73 | 74 | updateConfigInFixture(modifiedConfig); 75 | 76 | cy.mount(VueUiSmiley, { 77 | props: { 78 | dataset: fixture.dataset, 79 | config: modifiedConfig 80 | } 81 | }); 82 | 83 | for (let i = 0; i < 5; i += 1) { 84 | cy.get(`[data-cy="smiley-item-${i}"]`) 85 | .trigger('mouseenter') 86 | 87 | cy.get(`[data-cy="smiley-tooltip-${i}"]`) 88 | .should('exist') 89 | .contains(`${Object.keys(fixture.dataset.rating)[i]}: ${fixture.dataset.rating[i + 1]}`) 90 | } 91 | }); 92 | }); 93 | }) -------------------------------------------------------------------------------- /src/components/vue-ui-sparkbar.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiSparkbar from './vue-ui-sparkbar.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { dataLabel } from '../lib'; 4 | 5 | const { config, dataset } = components.find(c => c.name === 'VueUiSparkbar'); 6 | 7 | describe('<VueUiSparkbar />', () => { 8 | it('renders', () => { 9 | cy.mount(VueUiSparkbar, { 10 | props: { 11 | dataset, 12 | config 13 | } 14 | }).then(() => { 15 | 16 | for (let i = 0; i < dataset.length; i += 1) { 17 | cy.get(`[data-cy="sparkbar-svg-${i}"]`).should('exist'); 18 | cy.get(`[data-cy="sparkbar-name-${i}"]`) 19 | .should('exist') 20 | .contains(dataset[i].name); 21 | 22 | cy.get(`[data-cy="sparkbar-value-${i}"]`) 23 | .should('exist') 24 | .contains(dataLabel({ 25 | p: dataset[i].prefix, 26 | v: dataset[i].value, 27 | s: dataset[i].suffix, 28 | r: dataset[i].rounding 29 | })); 30 | 31 | if (config.style.layout.showTargetValue) { 32 | const targetValueText = config.style.layout.targetValueText; 33 | const target = config.style.layout.target ?? dataset[i].target ?? 0; 34 | 35 | const formattedValue = dataLabel({ 36 | p: dataset[i].prefix || '', 37 | v: target, 38 | s: dataset[i].suffix || '', 39 | r: dataset[i].rounding || 0 40 | }); 41 | 42 | const expectedText = `${targetValueText} ${formattedValue}`.trim(); 43 | 44 | cy.get(`[data-cy="sparkbar-target-value-${i}"]`) 45 | .should('exist') 46 | .invoke('text') 47 | .then((text) => { 48 | expect(text.trim()).to.eq(expectedText); 49 | }); 50 | } 51 | } 52 | }) 53 | }); 54 | 55 | it('renders with custom title and subtitle using slots', () => { 56 | const customTitle = 'Custom Title'; 57 | const customSubtitle = 'Custom Subtitle'; 58 | 59 | cy.mount(VueUiSparkbar, { 60 | props: { 61 | dataset, 62 | config 63 | }, 64 | slots: { 65 | title: `<div> 66 | <div data-cy="custom-title">${customTitle}</div> 67 | <div data-cy="custom-subtitle">${customSubtitle}</div> 68 | </div>` 69 | } 70 | }).then(() => { 71 | cy.get('[data-cy="custom-title"]').should('exist').contains(customTitle); 72 | cy.get('[data-cy="custom-subtitle"]').should('exist').contains(customSubtitle); 73 | cy.get('[data-cy="sparkbar-title-wrapper"]').should('not.exist'); 74 | }); 75 | }); 76 | 77 | it('renders with default title when no slot is provided', () => { 78 | cy.mount(VueUiSparkbar, { 79 | props: { 80 | dataset, 81 | config 82 | } 83 | }).then(() => { 84 | cy.get('[data-cy="sparkbar-title-wrapper"]').should('exist'); 85 | cy.get('[data-cy="sparkbar-title"]').should('exist').contains(config.style.title.text); 86 | cy.get('[data-cy="sparkbar-subtitle"]').should('exist').contains(config.style.title.subtitle.text); 87 | }); 88 | }); 89 | 90 | it('emits', () => { 91 | cy.mount(VueUiSparkbar, { 92 | props: { 93 | dataset, 94 | config 95 | } 96 | }).then(({ wrapper }) => { 97 | cy.log('@selectDatapoint'); 98 | cy.get('[data-cy="datapoint-wrapper"]').first().click({ force: true }).then(() => { 99 | expect(wrapper.emitted('selectDatapoint')[0][0]).to.have.keys('datapoint', 'index'); 100 | expect(wrapper.emitted('selectDatapoint')[0][0].index).to.equal(0); 101 | expect(wrapper.emitted('selectDatapoint')[0][0].datapoint).to.have.keys( 102 | 'color', 103 | 'formatter', 104 | 'name', 105 | 'prefix', 106 | 'rounding', 107 | 'suffix', 108 | 'value' 109 | ); 110 | }); 111 | }); 112 | }); 113 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-sparkhistogram.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiSparkhistogram from './vue-ui-sparkhistogram.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { dataLabel } from '../lib'; 4 | 5 | const { config, dataset } = components.find(c => c.name === 'VueUiSparkHistogram'); 6 | 7 | describe('<VueUiSparkHistogram />', () => { 8 | it('renders', () => { 9 | cy.mount(VueUiSparkhistogram, { 10 | props: { 11 | dataset, 12 | config 13 | } 14 | }).then(() => { 15 | cy.log('title'); 16 | cy.get('[data-cy="title"]').should('exist').and('be.visible').and('contain', config.style.title.text); 17 | cy.get('[data-cy="subtitle"]').should('exist').and('be.visible').and('contain', config.style.title.subtitle.text); 18 | 19 | cy.log('selection'); 20 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseover', { force: true }); 21 | cy.get('[data-cy="title-selection"]') 22 | .should('exist') 23 | .and('be.visible') 24 | .and('contain', dataset[0].timeLabel) 25 | .and('contain', dataLabel({ v: dataset[0].value })); 26 | cy.get('[data-cy="title-selection-value-label"]').should('exist').and('be.visible').and('contain', dataset[0].valueLabel); 27 | 28 | cy.log('datapoints'); 29 | cy.get('[data-cy="datapoint-rect"]').should('exist').and('be.visible').and('have.length', dataset.length); 30 | cy.get('[data-cy="datapoint-label-value"]').as('valueLabels').should('exist').and('be.visible').and('have.length', dataset.length); 31 | cy.get('@valueLabels').each((label, i) => { 32 | cy.wrap(label).contains(dataLabel({ v: dataset[i].value, r: config.style.labels.value.rounding })); 33 | }); 34 | cy.get('[data-cy="datapoint-label-valueLabel"]').as('valueLabels2').should('exist').and('be.visible').and('have.length', dataset.length); 35 | cy.get('@valueLabels2').each((label, i) => { 36 | cy.wrap(label).contains(dataset[i].valueLabel); 37 | }); 38 | cy.get('[data-cy="datapoint-label-time"]').as('timeLabels').should('exist').and('be.visible').and('have.length', dataset.length); 39 | cy.get('@timeLabels').each((label, i) => { 40 | cy.wrap(label).contains(dataset[i].timeLabel); 41 | }); 42 | }); 43 | }); 44 | 45 | it('emits', () => { 46 | cy.mount(VueUiSparkhistogram, { 47 | props: { 48 | dataset, 49 | config 50 | } 51 | }).then(({ wrapper }) => { 52 | cy.log('@selectDatapoint'); 53 | cy.get('[data-cy="tooltip-trap"]').first().click().then(() => { 54 | expect(wrapper.emitted('selectDatapoint')[0][0]).to.have.keys('index', 'datapoint'); 55 | expect(wrapper.emitted('selectDatapoint')[0][0].index).to.equal(0); 56 | expect(wrapper.emitted('selectDatapoint')[0][0].datapoint).to.have.keys( 57 | 'color', 58 | 'gradient', 59 | 'height', 60 | 'intensity', 61 | 'proportion', 62 | 'stroke', 63 | 'textAnchor', 64 | 'timeLabel', 65 | 'trapX', 66 | 'unitWidth', 67 | 'value', 68 | 'valueLabel', 69 | 'width', 70 | 'x', 71 | 'y' 72 | ) 73 | 74 | }) 75 | }) 76 | }) 77 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-sparkline.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiSparkline from './vue-ui-sparkline.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | 4 | const { config, dataset } = components.find(c => c.name === 'VueUiSparkline'); 5 | 6 | function commonTest(line=true) { 7 | cy.log('title'); 8 | cy.get('[data-cy="title"]').should('exist').and('be.visible').and('contain', config.style.title.text); 9 | 10 | cy.log('data label'); 11 | cy.get('[data-cy="sparkline-datalabel"]').should('exist').and('be.visible').and('contain', dataset.at(-1).value); 12 | 13 | cy.log('selection'); 14 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseenter', { force: true }); 15 | cy.get('[data-cy="selection-indicator"]').should('exist').and('have.css', 'opacity', '1'); 16 | cy.get('[data-cy="sparkline-datalabel"]').should('exist').and('be.visible').and('contain', dataset[0].value); 17 | cy.get('[data-cy="title"]').contains(dataset[0].period); 18 | if (line) { 19 | cy.get('[data-cy="selection-plot"]').should('exist').and('be.visible'); 20 | } 21 | } 22 | 23 | describe('<VueUiSparkline />', () => { 24 | it('renders (line)', () => { 25 | cy.mount(VueUiSparkline, { 26 | props: { 27 | dataset, 28 | config 29 | } 30 | }).then(() => { 31 | commonTest(); 32 | 33 | cy.log('area'); 34 | cy.get('[data-cy="sparkline-angle-area"]').should('exist').and('be.visible'); 35 | 36 | cy.log('line'); 37 | cy.get('[data-cy="sparkline-straight-line"]').should('exist').and('be.visible'); 38 | 39 | }); 40 | }); 41 | 42 | it('renders (spline)', () => { 43 | cy.mount(VueUiSparkline, { 44 | props: { 45 | dataset, 46 | config: { 47 | ...config, 48 | style: { 49 | ...config.style, 50 | line: { smooth: true } 51 | } 52 | } 53 | } 54 | }).then(() => { 55 | commonTest(); 56 | 57 | cy.log('area'); 58 | cy.get('[data-cy="sparkline-smooth-area"]').should('exist').and('be.visible'); 59 | 60 | cy.log('line'); 61 | cy.get('[data-cy="sparkline-smooth-path"]').should('exist').and('be.visible'); 62 | }); 63 | }); 64 | 65 | it('renders (bars)', () => { 66 | cy.mount(VueUiSparkline, { 67 | props: { 68 | dataset, 69 | config: { 70 | ...config, 71 | type: 'bar' 72 | } 73 | } 74 | }).then(() => { 75 | commonTest(false); 76 | cy.get('[data-cy="datapoint-bar"]').should('exist').and('be.visible').and('have.length', dataset.length); 77 | }); 78 | }); 79 | 80 | it('emits', () => { 81 | cy.mount(VueUiSparkline, { 82 | props: { 83 | dataset, 84 | config 85 | } 86 | }).then(({ wrapper }) => { 87 | cy.log('@hoverIndex'); 88 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseenter', { force: true }).then(() => { 89 | expect(wrapper.emitted('hoverIndex')[0][0]).to.have.keys('index'); 90 | expect(wrapper.emitted('hoverIndex')[0][0].index).to.equal(0); 91 | }).then(() => { 92 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseleave', { force: true}).then(() => { 93 | expect(wrapper.emitted('hoverIndex')[1][0]).to.have.keys('index'); 94 | expect(wrapper.emitted('hoverIndex')[1][0].index).to.equal(undefined) 95 | }); 96 | }); 97 | 98 | cy.log('@selectDatapoint'); 99 | cy.get('[data-cy="tooltip-trap"]').first().click({ force: true }).then(() => { 100 | expect(wrapper.emitted('selectDatapoint')[0][0]).to.have.keys('datapoint', 'index') 101 | expect(wrapper.emitted('selectDatapoint')[0][0].index).to.equal(0); 102 | expect(wrapper.emitted('selectDatapoint')[0][0].datapoint).to.have.keys( 103 | 'absoluteValue', 104 | 'color', 105 | 'id', 106 | 'period', 107 | 'plotValue', 108 | 'toMax', 109 | 'width', 110 | 'x', 111 | 'y' 112 | ); 113 | }); 114 | }); 115 | }); 116 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-sparkstackbar.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiSparkStackbar from './vue-ui-sparkstackbar.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { config, dataset } = components.find(c => c.name === 'VueUiSparkStackbar'); 6 | 7 | describe('<VueUiSparkStackbar />', () => { 8 | it('renders', () => { 9 | cy.mount(VueUiSparkStackbar, { 10 | props: { 11 | dataset, 12 | config 13 | } 14 | }).then(() => { 15 | testCommonFeatures({ 16 | title: true, 17 | subtitle: true, 18 | tooltipCallback: () => { 19 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseenter', { force: true }); 20 | } 21 | }); 22 | 23 | cy.log('datapoints'); 24 | cy.get('[data-cy="datapoint-underlayer"]').should('exist').and('be.visible').and('have.length', dataset.length); 25 | cy.get('[data-cy="datapoint"]').should('exist').and('be.visible').and('have.length', dataset.length); 26 | 27 | cy.log('legend'); 28 | cy.get('[data-cy="sparkstackbar-legend"]').should('exist').and('be.visible'); 29 | cy.get('[data-cy="legend-item"]').as('legendItems').should('exist').and('be.visible').and('have.length', dataset.length); 30 | cy.get('@legendItems').first().click(); 31 | cy.get('[data-cy="datapoint-underlayer"]').should('exist').and('be.visible').and('have.length', dataset.length - 1); 32 | cy.get('[data-cy="datapoint"]').should('exist').and('be.visible').and('have.length', dataset.length - 1); 33 | cy.get('@legendItems').first().click(); 34 | cy.get('[data-cy="datapoint-underlayer"]').should('exist').and('be.visible').and('have.length', dataset.length); 35 | cy.get('[data-cy="datapoint"]').should('exist').and('be.visible').and('have.length', dataset.length); 36 | }); 37 | }); 38 | 39 | it.only('emits', () => { 40 | cy.spy(window, 'requestAnimationFrame').as('rafSpy'); 41 | 42 | cy.mount(VueUiSparkStackbar, { 43 | props: { 44 | dataset, 45 | config 46 | } 47 | }).then(({ wrapper }) => { 48 | cy.log('@selectDatapoint'); 49 | cy.get('@rafSpy').should('have.been.called').then(() => { 50 | cy.get('[data-cy="tooltip-trap"]').first().click().then(() => { 51 | expect(wrapper.emitted('selectDatapoint')[0][0]).to.have.keys('datapoint', 'index'); 52 | expect(wrapper.emitted('selectDatapoint')[0][0].index).to.equal(0); 53 | expect(wrapper.emitted('selectDatapoint')[0][0].datapoint).to.have.keys('color', 'name', 'proportion', 'proportionLabel', 'start', 'value', 'width'); 54 | expect(wrapper.emitted('selectDatapoint')[0][0].datapoint.color).to.equal('#1f77b4ff'); 55 | expect(wrapper.emitted('selectDatapoint')[0][0].datapoint.name).to.equal(dataset[0].name); 56 | }); 57 | }); 58 | }); 59 | }); 60 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-stackbar.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiStackbar from "./vue-ui-stackbar.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiStackbar'); 6 | 7 | describe('<VueUiStackbar />', () => { 8 | 9 | function commonTest() { 10 | testCommonFeatures({ 11 | userOptions: true, 12 | title: true, 13 | subtitle: true, 14 | slicer: true, 15 | dataTable: true, 16 | legend: true, 17 | tooltipCallback: () => { 18 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseenter'); 19 | } 20 | }); 21 | 22 | cy.log('axes'); 23 | cy.get('[data-cy="line-axis-x"]').should('exist'); 24 | cy.get('[data-cy="line-axis-y"]').should('exist'); 25 | cy.get('[data-cy="axis-label-x"]').should('exist').and('be.visible').and('contain', config.style.chart.grid.x.axisName.text) 26 | cy.get('[data-cy="axis-label-y"]').should('exist').and('be.visible').and('contain', config.style.chart.grid.y.axisName.text) 27 | 28 | cy.log('scale labels'); 29 | cy.get('[data-cy="scale-line-y"]').should('exist').and('have.length', 9); 30 | cy.get('[data-cy="scale-label-y"]').as('yLabels').should('exist').and('be.visible').and('have.length', 9); 31 | cy.get('@yLabels').first().contains(-60) 32 | cy.get('@yLabels').last().contains(100) 33 | 34 | cy.log('time labels'); 35 | cy.get('[data-cy="time-label"]').as('timeLabels').should('exist').and('be.visible').and('have.length', Math.max(...dataset.map(d => d.series.length))); 36 | cy.get('@timeLabels').each((label, i) => { 37 | cy.wrap(label).as('label'); 38 | cy.get('@label').contains(i); 39 | }); 40 | 41 | cy.log('total labels'); 42 | cy.get('[data-cy="label-total"]').should('exist').and('be.visible').and('have.length', Math.max(...dataset.map(d => d.series.length))); 43 | 44 | cy.log('datapoint labels'); 45 | cy.get('[data-cy="label-datapoint"]').should('exist').and('be.visible'); 46 | } 47 | 48 | it('renders vertically', () => { 49 | cy.mount(VueUiStackbar, { 50 | props: { 51 | dataset, 52 | config 53 | } 54 | }).then(() => { 55 | commonTest(); 56 | }); 57 | }); 58 | 59 | it('renders horizontally', () => { 60 | cy.mount(VueUiStackbar, { 61 | props: { 62 | dataset, 63 | config: { ...config, orientation: 'horizontal' } 64 | } 65 | }).then(() => { 66 | commonTest(); 67 | }); 68 | }); 69 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-table-heatmap.cy.js: -------------------------------------------------------------------------------- 1 | import { testCommonFeatures } from "../../cypress/fixtures"; 2 | import { calcMedian } from "../lib"; 3 | import VueUiTableHeatmap from "./vue-ui-table-heatmap.vue"; 4 | import { h } from "vue"; 5 | 6 | describe('<VueUiTableHeatmap />', () => { 7 | 8 | const dataset = [ 9 | { 10 | name: "S1", 11 | values: [1, 1, 1], 12 | color: '#1f77b4', 13 | shape: 'circle' 14 | }, 15 | { 16 | name: "S2", 17 | values: [2, 2, 2], 18 | color: '#aec7e8', 19 | shape: 'triangle' 20 | }, 21 | { 22 | name: "S3", 23 | values: [3, 3, 3], 24 | color: '#ff7f0e', 25 | shape: 'diamond' 26 | }, 27 | ] 28 | 29 | const config = { 30 | table: { 31 | showSum: true, 32 | showAverage: true, 33 | showMedian: true, 34 | head: { 35 | values: ['A', 'B', 'C', 'D', 's', 'a', 'm'] 36 | } 37 | } 38 | } 39 | 40 | it('renders with slots', () => { 41 | cy.mount(VueUiTableHeatmap, { 42 | props: { 43 | dataset, 44 | config 45 | }, 46 | slots: { 47 | head: ({ value }) => h('div', { 'data-cy' : 'slot-head' }, value), 48 | rowTitle: ({ value }) => h('div', { 'data-cy' : 'slot-row-title' }, value), 49 | cell: ({ value, color, textColor }) => h('div', { 50 | 'data-cy': 'slot-cell', 51 | style: `background: ${color}; color: ${textColor}`, 52 | }, value), 53 | sum: ({ value }) => h('div', { 'data-cy': 'slot-sum' }, value), 54 | average: ({ value }) => h('div', { 'data-cy': 'slot-average' }, value.toFixed(0)), 55 | median: ({ value }) => h('div', { 'data-cy': 'slot-median' }, value.toFixed(0)), 56 | } 57 | }).then(() => { 58 | 59 | testCommonFeatures({ 60 | userOptions: true 61 | }); 62 | 63 | cy.log('head slot'); 64 | cy.get('[data-cy="slot-head"]').as('head').should('be.visible').and('have.length', config.table.head.values.length); 65 | cy.get('@head').each((th, i) => { 66 | cy.wrap(th).contains(config.table.head.values[i]); 67 | }); 68 | 69 | cy.log('rowTitle slot'); 70 | cy.get('[data-cy="slot-row-title"]').as('rowTitle').should('exist').and('be.visible'); 71 | cy.get('@rowTitle').each((rt, i) => { 72 | cy.wrap(rt).contains(dataset[i].name); 73 | }); 74 | 75 | cy.log('cell slot'); 76 | cy.get('[data-cy="slot-cell"]').should('exist').and('be.visible').and('have.length', dataset.length * dataset[0].values.length); 77 | 78 | cy.log('sum slot'); 79 | cy.get('[data-cy="slot-sum"]').as('sum').should('exist').and('be.visible').and('have.length', dataset.length); 80 | cy.get('@sum').each((s,i) => { 81 | cy.wrap(s).contains(dataset[i].values.reduce((a, b) => a + b, 0)); 82 | }); 83 | 84 | cy.log('average slot'); 85 | cy.get('[data-cy="slot-average"]').as('avg').should('exist').and('be.visible').and('have.length', dataset.length); 86 | cy.get('@avg').each((avg, i) => { 87 | const average = dataset[i].values.reduce((a, b) => a + b, 0) / dataset[i].values.length; 88 | cy.wrap(avg).contains(average.toFixed(0)); 89 | }); 90 | 91 | cy.log('median slot'); 92 | cy.get('[data-cy="slot-median"]').as('med').should('exist').and('be.visible').and('have.length', dataset.length); 93 | cy.get('@med').each((med, i) => { 94 | const median = calcMedian(dataset[i].values); 95 | cy.wrap(med).contains(median.toFixed(0)); 96 | }); 97 | }); 98 | }); 99 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-table-sparkline.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiTableSparkline from "./vue-ui-table-sparkline.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiTableSparkline'); 6 | 7 | describe('<VueUiTableSparkline />', () => { 8 | 9 | it('renders', () => { 10 | cy.mount(VueUiTableSparkline, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | 17 | testCommonFeatures({ 18 | userOptions: true, 19 | title: true, 20 | subtitle: true 21 | }); 22 | 23 | cy.get('[data-cy="th"]').should('exist').and('be.visible').and('have.length', 17); 24 | cy.get('[data-cy="tr"]').should('exist').and('have.length', dataset.length); 25 | cy.get('.vue-ui-sparkline').should('exist').and('be.visible').and('have.length', dataset.length); 26 | }); 27 | }); 28 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-thermometer.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiThermometer from './vue-ui-thermometer.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { config, dataset } = components.find(c => c.name === 'VueUiThermometer'); 6 | 7 | describe('<VueUiThermometer />', () => { 8 | it('renders', () => { 9 | cy.mount(VueUiThermometer, { 10 | props: { 11 | dataset, 12 | config 13 | } 14 | }).then(() => { 15 | testCommonFeatures({ 16 | userOptions: true, 17 | title: true, 18 | subtitle: true 19 | }); 20 | 21 | cy.log('pill'); 22 | cy.get('[data-cy="pill-underlayer"]').should('exist').and('be.visible'); 23 | cy.get('[data-cy="pill-graduation-rect"]').should('exist').and('be.visible').and('have.length', dataset.steps); 24 | cy.get('[data-cy="graduation-left"]').should('exist').and('have.css', 'opacity', '1').and('have.length', dataset.steps); 25 | cy.get('[data-cy="graduation-right"]').should('exist').and('have.css', 'opacity', '1').and('have.length', dataset.steps); 26 | cy.get('[data-cy="graduation-left-intermediary"]').should('exist').and('have.css', 'opacity', '1').and('have.length', dataset.steps * 3); 27 | cy.get('[data-cy="graduation-right-intermediary"]').should('exist').and('have.css', 'opacity', '1').and('have.length', dataset.steps * 3); 28 | 29 | cy.log('temperature'); 30 | cy.get('[data-cy="temperature-rect"]').should('exist').and('be.visible'); 31 | cy.get('[data-cy="temperature-label"]').should('exist').and('be.visible').and('contain', dataset.value); 32 | }); 33 | }); 34 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-tiremarks.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiTiremarks from "./vue-ui-tiremarks.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiTiremarks'); 6 | 7 | describe('<VueUiTiremarks />', () => { 8 | 9 | it('renders', () => { 10 | cy.spy(window, 'requestAnimationFrame').as('rafSpy'); 11 | cy.mount(VueUiTiremarks, { 12 | props: { 13 | dataset, 14 | config 15 | } 16 | }).then(() => { 17 | 18 | cy.log('Animating'); 19 | cy.get('@rafSpy').should('have.been.called'); 20 | 21 | testCommonFeatures({ 22 | userOptions: true, 23 | title: true, 24 | subtitle: true 25 | }); 26 | 27 | cy.log('ticks'); 28 | cy.get('[data-cy="tick"]').should('exist').and('have.css', 'opacity', '1').and('have.length', 100); 29 | 30 | cy.log('data label'); 31 | cy.get('[data-cy="data-label"]').should('exist').and('be.visible').and('contain', dataset.percentage).and('contain', '%'); 32 | }); 33 | }); 34 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-treemap.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiTreemap from "./vue-ui-treemap.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiTreemap'); 6 | 7 | describe('<VueUiTreemap />', () => { 8 | 9 | it('renders', () => { 10 | cy.mount(VueUiTreemap, { 11 | props: { 12 | dataset, 13 | config 14 | } 15 | }).then(() => { 16 | 17 | testCommonFeatures({ 18 | userOptions: true, 19 | title: true, 20 | subtitle: true, 21 | legend: true, 22 | dataTable: true, 23 | tooltipCallback: () => { 24 | cy.get('[data-cy="datapoint-rect"]').first().trigger('mouseenter', { force: true }) 25 | } 26 | }); 27 | 28 | cy.log('datapoint rects'); 29 | cy.get('[data-cy="datapoint-rect"]').should('exist').and('be.visible').and('have.length', 16); 30 | 31 | cy.log('datapoint foreignObject'); 32 | cy.get('.vue-ui-treemap-cell-foreignObject').should('exist').and('be.visible').and('have.length', 16); 33 | 34 | cy.log('zoom'); 35 | cy.get('[data-cy="datapoint-rect"]').first().click(); 36 | cy.get('[data-cy="datapoint-rect"]').should('exist').and('be.visible').and('have.length', 3); 37 | cy.get('[data-cy="datapoint-rect"]').first().click(); 38 | cy.get('[data-cy="datapoint-rect"]').should('exist').and('be.visible').and('have.length', 16); 39 | }); 40 | }); 41 | 42 | it('renders breadcrumbs', () => { 43 | cy.mount(VueUiTreemap, { 44 | props: { 45 | dataset, 46 | config 47 | } 48 | }).then(() => { 49 | cy.get('.vue-ui-treemap-breadcrumbs').should('not.exist') 50 | cy.get('.vue-ui-treemap-rect').first().click() 51 | cy.get('.vue-ui-treemap-breadcrumbs').should('exist').and('be.visible') 52 | cy.get('.vue-ui-treemap-crumb').first().click() 53 | cy.get('.vue-ui-treemap-breadcrumbs').should('not.exist') 54 | }) 55 | }) 56 | 57 | it('emits', () => { 58 | cy.mount(VueUiTreemap, { 59 | props: { 60 | dataset, 61 | config 62 | } 63 | }).then(({ wrapper }) => { 64 | cy.log('@selectLegend'); 65 | cy.get('[data-cy="legend-item-0"]').click().then(() => { 66 | expect(wrapper.emitted('selectLegend')).to.exist 67 | }); 68 | 69 | cy.log('@selectDatapoint'); 70 | cy.get('[data-cy="datapoint-rect"]').first().click({ force: true }).then(() => { 71 | expect(wrapper.emitted('selectDatapoint')).to.exist 72 | expect(wrapper.emitted('selectDatapoint')[0][0]).to.have.keys( 73 | 'children', 74 | 'color', 75 | 'id', 76 | 'name', 77 | 'normalizedValue', 78 | 'parentId', 79 | 'parentName', 80 | 'proportion', 81 | 'value', 82 | 'x0', 83 | 'x1', 84 | 'y0', 85 | 'y1' 86 | ); 87 | }); 88 | }); 89 | }); 90 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-vertical-bar.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiVerticalBar from './vue-ui-vertical-bar.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { config, dataset } = components.find(c => c.name === 'VueUiVerticalBar'); 6 | 7 | describe('<VueUiVerticalBar />', () => { 8 | it('renders', () => { 9 | cy.mount(VueUiVerticalBar, { 10 | props: { 11 | dataset, 12 | config 13 | } 14 | }).then(() => { 15 | 16 | testCommonFeatures({ 17 | userOptions: true, 18 | title: true, 19 | subtitle: true, 20 | dataTable: true, 21 | legend: true, 22 | tooltipCallback: () => { 23 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseenter', { force: true }) 24 | } 25 | }); 26 | 27 | cy.log('datapoints'); 28 | cy.get('[data-cy="datapoint-underlayer"]').should('exist').and('be.visible').and('have.length', 8); 29 | cy.get('[data-cy="datapoint-bar"]').should('exist').and('be.visible').and('have.length', 8); 30 | cy.get('[data-cy="datapoint-separator"]').should('exist').and('have.css', 'opacity', '1').and('have.length', 4); 31 | cy.get('[data-cy="datapoint-label"]').should('exist').and('be.visible').and('have.length', 8); 32 | cy.get('[data-cy="datapoint-name"]').should('exist').and('be.visible').and('have.length', 8); 33 | cy.get('[data-cy="datapoint-parent-name"]').should('exist').and('be.visible').and('have.length', dataset.filter(d => !!d.children).length); 34 | cy.get('[data-cy="datapoint-parent-value"]').should('exist').and('be.visible').and('have.length', dataset.filter(d => !!d.children).length); 35 | }); 36 | }); 37 | 38 | it('emits', () => { 39 | cy.mount(VueUiVerticalBar, { 40 | props: { 41 | dataset, 42 | config 43 | } 44 | }).then(({ wrapper }) => { 45 | cy.get('[data-cy="legend-item"]').first().click({ force: true }).then(() => { 46 | expect(wrapper.emitted('selectLegend')).to.exist; 47 | }); 48 | }); 49 | }); 50 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-waffle.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiWaffle from './vue-ui-waffle.vue'; 2 | import { components } from '../../cypress/fixtures/vdui-components'; 3 | import { testCommonFeatures } from '../../cypress/fixtures'; 4 | 5 | const { config, dataset } = components.find(c => c.name === 'VueUiWaffle'); 6 | 7 | describe('<VueUiWaffle />', () => { 8 | beforeEach(() => { 9 | cy.viewport(500,600); 10 | }) 11 | it('renders', () => { 12 | cy.mount(VueUiWaffle, { 13 | props: { 14 | dataset, 15 | config 16 | } 17 | }).then(() => { 18 | 19 | testCommonFeatures({ 20 | userOptions: true, 21 | title: true, 22 | subtitle: true, 23 | legend: true, 24 | dataTable: true, 25 | tooltipCallback: () => { 26 | cy.get('[data-cy="tooltip-trap"]').first().trigger('mouseover', { force: true }); 27 | } 28 | }); 29 | 30 | cy.log('datapoints'); 31 | cy.get('[data-cy="datapoint-underlayer"]').should('exist').and('be.visible').and('have.length', config.style.chart.layout.grid.size ** 2); 32 | cy.get('[data-cy="datapoint-rect"]').should('exist').and('be.visible').and('have.length', config.style.chart.layout.grid.size ** 2); 33 | cy.get('[data-cy="datapoint-caption"]').as('captions').should('exist').and('be.visible').and('have.length', dataset.length); 34 | cy.get('@captions').each((caption, i) => { 35 | cy.wrap(caption) 36 | .should('contain', dataset[i].name) 37 | .and('contain', dataset[i].values.reduce((a, b) => a + b, 0)) 38 | }); 39 | }); 40 | }); 41 | 42 | it('emits', () => { 43 | cy.mount(VueUiWaffle, { 44 | props: { 45 | dataset, 46 | config 47 | } 48 | }).then(({ wrapper }) => { 49 | cy.spy(window, 'requestAnimationFrame').as('rafSpy'); 50 | 51 | cy.log('@selectLegend'); 52 | cy.get('[data-cy="legend-item"]').first().click({ force: true }).then(() => { 53 | expect(wrapper.emitted('selectLegend')).to.exist; 54 | cy.get('@rafSpy').should('have.been.called'); 55 | }); 56 | }); 57 | }); 58 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-wheel.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiWheel from "./vue-ui-wheel.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiWheel'); 6 | 7 | describe('<VueUiWheel />', () => { 8 | 9 | it('renders', () => { 10 | cy.viewport(500, 560); 11 | cy.spy(window, 'requestAnimationFrame').as('rafSpy'); 12 | 13 | cy.mount(VueUiWheel, { 14 | props: { 15 | dataset, 16 | config 17 | } 18 | }).then(() => { 19 | 20 | cy.log('Animating'); 21 | cy.get('@rafSpy').should('have.been.called'); 22 | 23 | testCommonFeatures({ 24 | userOptions: true, 25 | title: true, 26 | subtitle: true 27 | }); 28 | 29 | cy.log('ticks'); 30 | cy.get('[data-cy="wheel-tick"]').should('exist').and('be.visible').and('have.length', 100); 31 | 32 | cy.log('inner circle'); 33 | cy.get('[data-cy="inner-circle"]').should('exist').and('be.visible'); 34 | 35 | cy.log('data label'); 36 | cy.get('[data-cy="data-label"]').should('exist').and('be.visible').and('contain', dataset.percentage).and('contain', '%'); 37 | }); 38 | }); 39 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-word-cloud.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiWordCloud from "./vue-ui-word-cloud.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiWordCloud'); 6 | 7 | describe('<VueUiWordCloud />', () => { 8 | 9 | it('renders', () => { 10 | cy.viewport(500, 570); 11 | cy.mount(VueUiWordCloud, { 12 | props: { 13 | dataset, 14 | config 15 | } 16 | }).then(() => { 17 | 18 | testCommonFeatures({ 19 | userOptions: true, 20 | title: true, 21 | subtitle: true, 22 | dataTable: true, 23 | tooltipCallback: () => { 24 | cy.get('[data-cy="datapoint-word"]').first().trigger('mouseover', { force: true }) 25 | } 26 | }); 27 | 28 | cy.get('[data-cy="datapoint-word"]').should('exist').and('be.visible').and('have.length', 5); 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /src/components/vue-ui-xy-canvas.cy.js: -------------------------------------------------------------------------------- 1 | import VueUiXyCanvas from "./vue-ui-xy-canvas.vue"; 2 | import { components } from "../../cypress/fixtures/vdui-components"; 3 | import { testCommonFeatures } from "../../cypress/fixtures"; 4 | 5 | const { dataset, config } = components.find(c => c.name === 'VueUiXyCanvas'); 6 | 7 | describe('<VueUiXyCanvas />', () => { 8 | 9 | it('renders', () => { 10 | cy.viewport(500, 560); 11 | cy.mount(VueUiXyCanvas, { 12 | props: { 13 | dataset, 14 | config 15 | } 16 | }).then(() => { 17 | 18 | testCommonFeatures({ 19 | userOptions: true, 20 | title: true, 21 | subtitle: true, 22 | legend: true, 23 | dataTable: true, 24 | slicer: true, 25 | tooltipCallback: () => { 26 | cy.get('[data-cy="canvas"]').trigger('mousemove') 27 | } 28 | }); 29 | 30 | cy.get('[data-cy="canvas"]').then(canvas => { 31 | const c = canvas[0]; 32 | const ctx = c.getContext('2d'); 33 | const pixelData = ctx.getImageData(10, 10, 1, 1).data; 34 | expect(pixelData[3]).to.be.greaterThan(0); 35 | }); 36 | }); 37 | }); 38 | 39 | it('emits', () => { 40 | cy.mount(VueUiXyCanvas, { 41 | props: { 42 | dataset, 43 | config 44 | } 45 | }).then(({ wrapper }) => { 46 | cy.log('@selectLegend'); 47 | cy.get('[data-cy="legend-item"]').first().click({ force: true }).then(() => { 48 | expect(wrapper.emitted('selectLegend')).to.exist; 49 | }); 50 | }); 51 | }); 52 | }); -------------------------------------------------------------------------------- /src/directives/vClickOutside.js: -------------------------------------------------------------------------------- 1 | export default { 2 | beforeMount(el, binding) { 3 | el.clickOutsideEvent = function(event) { 4 | if (!(el === event.target || el.contains(event.target))) { 5 | binding.value(event); 6 | } 7 | }; 8 | document.addEventListener('click', el.clickOutsideEvent); 9 | }, 10 | unmounted(el) { 11 | document.removeEventListener('click', el.clickOutsideEvent); 12 | }, 13 | }; -------------------------------------------------------------------------------- /src/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataset": "#COMP# dataset prop is either missing, undefined or empty.", 3 | "datasetAttribute": "#COMP# dataset is missing the '#ATTR#' attribute.", 4 | "datasetAttributeEmpty": "#COMP# dataset '#ATTR#' attribute cannot be empty.", 5 | "datasetSerieAttribute": "#COMP# dataset #KEY# item at index #INDX# is missing the '#ATTR#' attribute.", 6 | "notBuildable": "#COMP# : Chart could not be built. Dataset is not formatted correctly", 7 | "attributeWrongValue": "#COMP# : A wrong value was provided to the #ATTR# attribute (#KEY# is not a valid value)." 8 | } -------------------------------------------------------------------------------- /src/event.js: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted } from "vue"; 2 | 3 | export function useEventListener(target, event, callback) { 4 | onMounted(() => target.addEventListener(event, callback)); 5 | onUnmounted(() => target.removeEventListener(event, callback)); 6 | } -------------------------------------------------------------------------------- /src/exposedLib.js: -------------------------------------------------------------------------------- 1 | import { convertColorToHex, darkenHexColor, lightenHexColor, shiftHue } from "./lib"; 2 | 3 | export function lightenColor(color, strength) { 4 | const hexColor = convertColorToHex(color); 5 | return lightenHexColor(hexColor, strength); 6 | } 7 | 8 | export function darkenColor(color, strength) { 9 | const hexColor = convertColorToHex(color); 10 | return darkenHexColor(hexColor, strength); 11 | } 12 | 13 | export function shiftColorHue(color, strength) { 14 | const hexColor = convertColorToHex(color); 15 | return shiftHue(hexColor, strength); 16 | } 17 | 18 | const exposedLib = { 19 | lightenColor, 20 | darkenColor, 21 | shiftColorHue 22 | } 23 | 24 | export default exposedLib; -------------------------------------------------------------------------------- /src/getThemeConfig.js: -------------------------------------------------------------------------------- 1 | import themes from "./themes.json"; 2 | 3 | export default function getThemeConfig(type) { 4 | return themes[type] 5 | } -------------------------------------------------------------------------------- /src/getVueDataUiConfig.js: -------------------------------------------------------------------------------- 1 | import { useConfig } from "./useConfig"; 2 | 3 | export default function getVueDataUiConfig(type) { 4 | return useConfig()[type] 5 | } -------------------------------------------------------------------------------- /src/img.js: -------------------------------------------------------------------------------- 1 | import { domToPng } from "./dom-to-png"; 2 | 3 | export default async function img({ domElement, fileName, format = 'png', scale = 2 }) { 4 | if (!domElement) return Promise.reject('No element provided'); 5 | 6 | if (format === 'svg') { 7 | const imgEl = converter.convertToImg({ container: domElement, scale }); 8 | if (!imgEl) return Promise.reject('Could not create SVG image'); 9 | const link = document.createElement('a'); 10 | link.href = imgEl.src; 11 | link.download = `${fileName}.svg`; 12 | document.body.appendChild(link); 13 | link.click(); 14 | document.body.removeChild(link); 15 | return Promise.resolve(); 16 | } 17 | 18 | try { 19 | const imageDataUrl = await domToPng({ container: domElement, scale }); 20 | const link_1 = document.createElement('a'); 21 | link_1.href = imageDataUrl; 22 | link_1.download = `${fileName}.${format}`; 23 | document.body.appendChild(link_1); 24 | link_1.click(); 25 | document.body.removeChild(link_1); 26 | } catch (err) { 27 | console.error("Error generating image:", err); 28 | throw err; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pdf.js: -------------------------------------------------------------------------------- 1 | import { domToPng } from "./dom-to-png"; 2 | 3 | export default async function pdf({ domElement, fileName, scale = 2, options = {} }) { 4 | if (!domElement) return Promise.reject("No domElement provided"); 5 | 6 | let JsPDF; 7 | 8 | try { 9 | JsPDF = (await import('jspdf')).default; 10 | } catch (e) { 11 | return Promise.reject('jspdf is not installed. Run npm install jspdf') 12 | } 13 | 14 | const a4 = { 15 | width: 595.28, 16 | height: 841.89, 17 | }; 18 | 19 | const imgData = await domToPng({ container: domElement, scale }); 20 | return await new Promise((resolve, reject) => { 21 | const img = new window.Image(); 22 | img.onload = function () { 23 | const contentWidth = img.naturalWidth; 24 | const contentHeight = img.naturalHeight; 25 | 26 | let imgWidth = a4.width; 27 | let imgHeight = (a4.width / contentWidth) * contentHeight; 28 | 29 | const pdf = new JsPDF("", "pt", "a4"); 30 | let position = 0; 31 | let leftHeight = contentHeight; 32 | const pageHeight = (contentWidth / a4.width) * a4.height; 33 | 34 | if (leftHeight < pageHeight) { 35 | pdf.addImage( 36 | imgData, 37 | "PNG", 38 | 0, 39 | 0, 40 | imgWidth, 41 | imgHeight, 42 | "", 43 | "FAST" 44 | ); 45 | } else { 46 | while (leftHeight > 0) { 47 | pdf.addImage( 48 | imgData, 49 | "PNG", 50 | 0, 51 | position, 52 | imgWidth, 53 | imgHeight, 54 | "", 55 | "FAST" 56 | ); 57 | leftHeight -= pageHeight; 58 | position -= a4.height; 59 | if (leftHeight > 0) { 60 | pdf.addPage(); 61 | } 62 | } 63 | } 64 | pdf.save(`${fileName}.pdf`); 65 | resolve(); 66 | }; 67 | img.onerror = err => reject("Failed to load image for PDF: " + err); 68 | img.src = imgData; 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | background-color: #1A1A1A; 6 | 7 | 8 | font-synthesis: none; 9 | text-rendering: optimizeLegibility; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | -webkit-text-size-adjust: 100%; 13 | } -------------------------------------------------------------------------------- /src/useArena.js: -------------------------------------------------------------------------------- 1 | import { ref} from "vue"; 2 | 3 | export function useArena() { 4 | const local = ref(null); 5 | const build = ref(null); 6 | const vduiLocal = ref(null); 7 | const vduiBuild = ref(null); 8 | 9 | function toggleLabels() { 10 | local.value.toggleLabels(); 11 | build.value.toggleLabels(); 12 | vduiLocal.value.toggleLabels(); 13 | vduiBuild.value.toggleLabels(); 14 | } 15 | 16 | function toggleTable() { 17 | local.value.toggleTable(); 18 | build.value.toggleTable(); 19 | vduiLocal.value.toggleTable(); 20 | vduiBuild.value.toggleTable(); 21 | } 22 | 23 | function toggleSort() { 24 | local.value.toggleSort(); 25 | build.value.toggleSort(); 26 | vduiLocal.value.toggleSort(); 27 | vduiBuild.value.toggleSort(); 28 | } 29 | 30 | function toggleStack() { 31 | local.value.toggleStack(); 32 | build.value.toggleStack(); 33 | vduiLocal.value.toggleStack(); 34 | vduiBuild.value.toggleStack(); 35 | } 36 | 37 | return { 38 | toggleLabels, 39 | toggleTable, 40 | toggleSort, 41 | toggleStack, 42 | local, 43 | build, 44 | vduiLocal, 45 | vduiBuild 46 | } 47 | } -------------------------------------------------------------------------------- /src/useChartAccessibility.js: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, nextTick } from "vue"; 2 | 3 | export function useChartAccessibility({ config }) { 4 | const svgRef = ref(null); 5 | 6 | const titleText = config?.text || "Chart visualization"; 7 | const subtitleText = config?.subtitle?.text || ""; 8 | 9 | onMounted(() => { 10 | nextTick(() => { 11 | if (svgRef.value) { 12 | svgRef.value.setAttribute("aria-label", `${titleText}${subtitleText ? `. ${subtitleText}` : ''}`); 13 | svgRef.value.setAttribute("role", "img"); 14 | svgRef.value.setAttribute("aria-live", "polite"); 15 | } 16 | }) 17 | }); 18 | 19 | return { svgRef }; 20 | } -------------------------------------------------------------------------------- /src/useMouse.js: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import { useEventListener } from "./event"; 3 | 4 | export function useMouse() { 5 | const x = ref(0); 6 | const y = ref(0); 7 | 8 | useEventListener(window, 'mousemove', (event) => { 9 | x.value = event.clientX, 10 | y.value = event.clientY 11 | }); 12 | 13 | return { x, y } 14 | } -------------------------------------------------------------------------------- /src/useNestedProp.js: -------------------------------------------------------------------------------- 1 | import { treeShake, convertConfigColors } from "./lib"; 2 | 3 | export function useNestedProp({ defaultConfig, userConfig }) { 4 | if(!Object.keys(userConfig || {}).length) { 5 | return defaultConfig; 6 | } 7 | const reconciled = treeShake({ 8 | defaultConfig: defaultConfig, 9 | userConfig 10 | }); 11 | return convertConfigColors(reconciled) 12 | } -------------------------------------------------------------------------------- /src/usePrinter.js: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | export function usePrinter({ 4 | elementId, 5 | fileName, 6 | canPrint = true, 7 | options 8 | }) { 9 | const isPrinting = ref(false); 10 | const isImaging = ref(false); 11 | const __to__ = ref(null); 12 | 13 | async function generatePdf() { 14 | if (!canPrint || isPrinting.value) return; 15 | isPrinting.value = true; 16 | clearTimeout(__to__.value); 17 | __to__.value = setTimeout(async () => { 18 | if (canPrint) { 19 | try { 20 | const { default: pdf } = await import("./pdf"); 21 | await pdf({ 22 | domElement: document.getElementById(elementId), 23 | fileName, 24 | options 25 | }); 26 | } catch (error) { 27 | console.error("Error generating PDF:", error); 28 | } finally { 29 | isPrinting.value = false; 30 | } 31 | } 32 | }, 100); 33 | } 34 | 35 | async function generateImage() { 36 | if (!canPrint || isImaging.value) return; 37 | isImaging.value = true; 38 | clearTimeout(__to__.value); 39 | __to__.value = setTimeout(async () => { 40 | if (canPrint) { 41 | try { 42 | const { default: img } = await import("./img"); 43 | await img({ 44 | domElement: document.getElementById(elementId), 45 | fileName, 46 | format: "png", 47 | options 48 | }); 49 | } catch (error) { 50 | console.error("Error generating image:", error); 51 | } finally { 52 | isImaging.value = false; 53 | } 54 | } 55 | }, 100); 56 | } 57 | 58 | return { 59 | generatePdf, 60 | generateImage, 61 | isPrinting, 62 | isImaging, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/useResponsive.js: -------------------------------------------------------------------------------- 1 | export function useResponsive({ 2 | chart, 3 | title = null, 4 | slicer = null, 5 | legend = null, 6 | source = null, 7 | noTitle = null, 8 | padding = null 9 | }) { 10 | let height = 0; 11 | let width = 0; 12 | let heightTitle = 0; 13 | let heightSlicer = 0; 14 | let heightLegend = 0; 15 | let heightSource = 0; 16 | let heightNoTitle = 0; 17 | let heightPadding = 0; 18 | let widthPadding = 0; 19 | 20 | if (!!chart) { 21 | const parent = chart.parentNode; 22 | const { height:parentHeight, width: parentWidth } = parent.getBoundingClientRect(); 23 | 24 | if (title) { 25 | heightTitle = title.getBoundingClientRect().height; 26 | } 27 | if (slicer) { 28 | heightSlicer = slicer.getBoundingClientRect().height; 29 | } 30 | if (legend) { 31 | heightLegend = legend.getBoundingClientRect().height; 32 | } 33 | if (source) { 34 | heightSource = source.getBoundingClientRect().height; 35 | } 36 | if (noTitle) { 37 | heightNoTitle = noTitle.getBoundingClientRect().height; 38 | } 39 | if (padding) { 40 | heightPadding = padding.top + padding.bottom; 41 | widthPadding = padding.right + padding.left; 42 | } 43 | 44 | height = parentHeight 45 | - heightTitle 46 | - heightSlicer 47 | - heightLegend 48 | - heightSource 49 | - heightNoTitle 50 | - heightPadding; 51 | 52 | width = parentWidth 53 | - widthPadding; 54 | } 55 | 56 | return { 57 | width, 58 | height, 59 | heightTitle, 60 | heightNoTitle 61 | } 62 | } -------------------------------------------------------------------------------- /src/useUserOptionState.js: -------------------------------------------------------------------------------- 1 | import { computed, ref } from "vue"; 2 | 3 | export function useUserOptionState({ 4 | config 5 | }) { 6 | const showUserOptionsOnChartHover = computed(() => config.userOptions.showOnChartHover); 7 | const keepUserOptionState = computed(() => config.userOptions.keepStateOnChartLeave); 8 | const userOptionsVisible = ref(!config.userOptions.showOnChartHover); 9 | 10 | function setUserOptionsVisibility(state = false) { 11 | if (!showUserOptionsOnChartHover.value) return; 12 | userOptionsVisible.value = state; 13 | } 14 | 15 | return { 16 | userOptionsVisible, 17 | keepUserOptionState, 18 | setUserOptionsVisibility 19 | } 20 | } -------------------------------------------------------------------------------- /src/vue-data-ui.css: -------------------------------------------------------------------------------- 1 | /* COMMON SELECTORS */ 2 | 3 | .vue-ui-dna * { 4 | animation: none !important; 5 | } 6 | 7 | .vue-data-ui-fullscreen--on { 8 | height: 90%; 9 | margin: 0 auto !important; 10 | } 11 | .vue-data-ui-fullscreen--off { 12 | max-width: 100%; 13 | } 14 | .vue-data-ui-wrapper-fullscreen { 15 | overflow: scroll; 16 | } 17 | 18 | .vue-data-ui-zoom-plus { 19 | cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' height='24' width='24'><path fill='none' d='M 14.65 14.65 L 18 18' stroke-linejoin='round' stroke-width='2' stroke='white'/><path fill='none' stroke-width='1' stroke='black' d='M 9 1 A 1 1 0 0 0 9 17 A 1 1 0 0 0 9 1 M 14.65 14.65 L 18 18 M 9 5 L 9 13 M 5 9 L 13 9'/><path fill='none' d='M 9 0 A 1 1 0 0 0 9 18 A 1 1 0 0 0 9 0' stroke-width='0.5' stroke='white'/><path fill='none' d='M 4 8 L 8 8 L 8 4 L 10 4 L 10 8 L 14 8 L 14 10 L 10 10 L 10 14 L 8 14 L 8 10 L 4 10 Z' stroke-linejoin='round' stroke-width='0.5' stroke='white'/></svg>") 10 10, pointer; 20 | } 21 | 22 | .vue-data-ui-zoom-minus { 23 | cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' height='24' width='24'><path fill='none' d='M 14.65 14.65 L 18 18' stroke-linejoin='round' stroke-width='2' stroke='white'/><path fill='none' stroke-width='1' stroke='black' d='M 9 1 A 1 1 0 0 0 9 17 A 1 1 0 0 0 9 1 M 14.65 14.65 L 18 18 M 5 9 L 13 9'/><path fill='none' d='M 9 0 A 1 1 0 0 0 9 18 A 1 1 0 0 0 9 0' stroke-width='0.5' stroke='white'/><path fill='none' d='M 4 8 L 14 8 L 14 10 L 4 10 Z' stroke-linejoin='round' stroke-width='0.5' stroke='white'/></svg>") 10 10, pointer; 24 | } 25 | 26 | .vue-data-ui-watermark { 27 | position: absolute; 28 | top: 50%; 29 | left: 50%; 30 | transform: translate(-50%, -50%); 31 | pointer-events: none; 32 | } -------------------------------------------------------------------------------- /star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphieros/vue-data-ui/9f13dde454ea583b09396403eec7f913d90e5e96/star.png -------------------------------------------------------------------------------- /tests/getThemeConfig.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe } from "vitest"; 2 | import sut from '../src/themes.json' 3 | import getThemeConfig from "../src/getThemeConfig"; 4 | 5 | const components = [ 6 | '3d_bar', 7 | 'age_pyramid', 8 | 'candlestick', 9 | 'chestnut', 10 | 'donut', 11 | 'donut_evolution', 12 | 'dumbbell', 13 | 'galaxy', 14 | 'gauge', 15 | 'heatmap', 16 | 'molecule', 17 | 'mood_radar', 18 | 'nested_donuts', 19 | 'onion', 20 | 'parallel_coordinate_plot', 21 | 'quadrant', 22 | 'quick_chart', 23 | 'radar', 24 | 'relation_circle', 25 | 'rings', 26 | 'scatter', 27 | 'spark_trend', 28 | 'sparkbar', 29 | 'sparkgauge', 30 | 'sparkhistogram', 31 | 'sparkline', 32 | 'sparkstackbar', 33 | 'strip_plot', 34 | 'table_heatmap', 35 | 'table_sparkline', 36 | 'thermometer', 37 | 'tiremarks', 38 | 'treemap', 39 | 'vertical_bar', 40 | 'waffle', 41 | 'wheel', 42 | 'xy', 43 | 'xy_canvas' 44 | ] 45 | 46 | const themes = [ 47 | "default", 48 | "zen", 49 | "hack", 50 | "concrete" 51 | ] 52 | 53 | describe('getThemeConfig', () => { 54 | components.forEach(component => { 55 | themes.forEach(theme => { 56 | test(`returns vue_ui_${component} ${theme} theme`, () => { 57 | expect(getThemeConfig(`vue_ui_${component}`)[theme]).not.toBeUndefined() 58 | expect(getThemeConfig(`vue_ui_${component}`)[theme]).toStrictEqual(sut[`vue_ui_${component}`][theme]) 59 | }) 60 | }) 61 | }) 62 | }) -------------------------------------------------------------------------------- /tests/getVueDataUiConfig.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe } from "vitest"; 2 | import getVueDataUiConfig from "../src/getVueDataUiConfig"; 3 | import { useConfig } from "../src/useConfig"; 4 | 5 | describe('getVueDataUiConfig', () => { 6 | 7 | const components = [ 8 | "heatmap", 9 | '3d_bar', 10 | 'accordion', 11 | 'age_pyramid', 12 | 'annotator', 13 | 'candlestick', 14 | 'chestnut', 15 | 'cursor', 16 | 'dashboard', 17 | 'digits', 18 | 'donut', 19 | 'donut_evolution', 20 | 'dumbbell', 21 | 'galaxy', 22 | 'gauge', 23 | 'kpi', 24 | 'mini_loader', 25 | 'molecule', 26 | 'mood_radar', 27 | 'nested_donuts', 28 | 'onion', 29 | 'quadrant', 30 | 'quick_chart', 31 | 'radar', 32 | 'rating', 33 | 'relation_circle', 34 | 'rings', 35 | 'scatter', 36 | 'skeleton', 37 | 'smiley', 38 | 'spark_trend', 39 | 'sparkbar', 40 | 'sparkgauge', 41 | 'sparkhistogram', 42 | 'sparkline', 43 | 'sparkstackbar', 44 | 'strip_plot', 45 | 'table', 46 | 'table_heatmap', 47 | 'table_sparkline', 48 | 'thermometer', 49 | 'tiremarks', 50 | 'vertical_bar', 51 | 'waffle', 52 | 'wheel', 53 | 'xy', 54 | ] 55 | 56 | components.forEach(component => { 57 | test(`returns vue_ui_${component} config`, () => { 58 | const expectedConfig = useConfig()[`vue_ui_${component}`] 59 | expect(getVueDataUiConfig(`vue_ui_${component}`)).not.toBeUndefined(); 60 | expect(getVueDataUiConfig(`vue_ui_${component}`)).toStrictEqual(expectedConfig); 61 | }) 62 | }) 63 | }) -------------------------------------------------------------------------------- /tests/useNestedProp.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe } from "vitest"; 2 | import { useNestedProp } from "../src/useNestedProp"; 3 | 4 | describe('useNestedProp', () => { 5 | test('returns reconcilied config object with converted colors', () => { 6 | const defaultConfig = { 7 | attr1: { 8 | color: "#FFFFFF", 9 | value: 12, 10 | someDefaultObject: { 11 | defaultAttr: 'default' 12 | } 13 | }, 14 | attr2: { 15 | color: '#000000' 16 | } 17 | } 18 | 19 | const userConfig = { 20 | attr1: { 21 | color: "rgb(0,0,0)", 22 | value: 1 23 | }, 24 | attr2: { 25 | color: 'red' 26 | } 27 | } 28 | 29 | expect(useNestedProp({ defaultConfig, userConfig })).toStrictEqual({ 30 | attr1: { 31 | color: "#000000ff", 32 | value: 1, 33 | someDefaultObject: { 34 | defaultAttr: "default" 35 | }, 36 | }, 37 | attr2: { 38 | color: "#FF0000ff" 39 | } 40 | }) 41 | }) 42 | }) -------------------------------------------------------------------------------- /vite.config.cypress.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | 4 | export default defineConfig({ 5 | plugins: [vue()], 6 | resolve: { 7 | alias: { 8 | 'vue': 'vue/dist/vue.esm-bundler.js' 9 | } 10 | }, 11 | optimizeDeps: { 12 | include: ['vue-data-ui'] 13 | }, 14 | }); -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | import vue from "@vitejs/plugin-vue"; 4 | import removeAttr from 'remove-attr'; 5 | import fs from "fs"; 6 | 7 | const prod = process.env.NODE_ENV === 'production'; 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | vue(), 13 | removeAttr({ 14 | extensions: [ 'vue' ], 15 | attributes: prod ? [ 'data-cy' ] : [], 16 | }), 17 | { 18 | name: "copy-llms-file", 19 | closeBundle() { 20 | const src = resolve(__dirname, "llms.txt"); 21 | const dest = resolve(__dirname, "dist", "llms.txt"); 22 | fs.copyFileSync(src, dest); 23 | console.log("llms.txt copied to dist folder."); 24 | }, 25 | }, 26 | ], 27 | build: { 28 | lib: { 29 | // src/indext.ts is where we have exported the component(s) 30 | entry: resolve(__dirname, "src/index.js"), 31 | name: "VueDataUi", 32 | // the name of the output files when the build is run 33 | fileName: "vue-data-ui", 34 | formats: ['es', 'cjs'] 35 | }, 36 | rollupOptions: { 37 | // make sure to externalize deps that shouldn't be bundled 38 | // into your library 39 | external: ["vue", "vue-data-ui", "jspdf"], 40 | output: { 41 | // Provide global variables to use in the UMD build 42 | // for externalized deps 43 | globals: { 44 | vue: "Vue", 45 | }, 46 | }, 47 | }, 48 | types: [ 49 | { 50 | declarationDir: "dist/types", 51 | root: resolve(__dirname, "types"), 52 | entry: "vue-data-ui.d.ts", 53 | }, 54 | ], 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /vue-data-ui-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphieros/vue-data-ui/9f13dde454ea583b09396403eec7f913d90e5e96/vue-data-ui-logo.png --------------------------------------------------------------------------------