From f873b98b15e95694cbaf085269fb0626b94ffa9e Mon Sep 17 00:00:00 2001 From: graphieros Date: Wed, 9 Jul 2025 14:40:47 +0200 Subject: [PATCH 1/8] Internal lib - Add methods to create tspans --- src/lib.js | 23 ++++++- tests/lib.test.js | 166 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 181 insertions(+), 8 deletions(-) diff --git a/src/lib.js b/src/lib.js index c51649f1..5afa2868 100755 --- a/src/lib.js +++ b/src/lib.js @@ -1603,6 +1603,25 @@ export function calcTrend(numbers) { return averagePercentageChange; } +export function createTSpansFromLineBreaksOnX({ content, fontSize, fill, x, y }) { + const lines = content.split('\n'); + return lines + .map((line, idx) => + `${line}` + ) + .join(''); +} + +export function createTSpansFromLineBreaksOnY({ content, fontSize, fill, x }) { + const lines = content.split('\n'); + return lines + .map((line, idx) => { + const dy = idx === 0 ? 0 : fontSize * 1.3; + return `${line}`; + }) + .join(''); +} + export function createTSpans({ content, fontSize, @@ -2622,6 +2641,8 @@ const lib = { createStar, createStraightPath, createTSpans, + createTSpansFromLineBreaksOnX, + createTSpansFromLineBreaksOnY, createUid, createWordCloudDatasetFromPlainText, darkenHexColor, @@ -2676,6 +2697,6 @@ const lib = { createAreaWithCuts, createIndividualAreaWithCuts, createSmoothAreaSegments, - createIndividualArea + createIndividualArea, }; export default lib; \ No newline at end of file diff --git a/tests/lib.test.js b/tests/lib.test.js index f1eae3ee..5ab95e18 100644 --- a/tests/lib.test.js +++ b/tests/lib.test.js @@ -36,6 +36,8 @@ import { createSpiralPath, createStar, createTSpans, + createTSpansFromLineBreaksOnX, + createTSpansFromLineBreaksOnY, createWordCloudDatasetFromPlainText, checkFormatter, darkenHexColor, @@ -3460,11 +3462,11 @@ describe('getCumulativeAverage', () => { const valid = [0, 1, 2, 3, 1]; const invalid = [0, 1, NaN, undefined, null, Infinity, -Infinity, 2, 3, 1]; test('returns cumulative average for a complete array of numbers', () => { - expect(getCumulativeAverage({values: valid})).toEqual([0, 0.5, 1, 1.5, 1.4]); + expect(getCumulativeAverage({ values: valid })).toEqual([0, 0.5, 1, 1.5, 1.4]); }); test('returns cumulative average and invalid values, but invalid values ignored in average', () => { - expect(getCumulativeAverage({values: invalid})).toEqual([0, 0.5, NaN, undefined, null, Infinity, -Infinity, 1 , 1.5, 1.4]); + expect(getCumulativeAverage({ values: invalid })).toEqual([0, 0.5, NaN, undefined, null, Infinity, -Infinity, 1, 1.5, 1.4]); }); test('returns cumulative average without invalid values', () => { @@ -3474,7 +3476,7 @@ describe('getCumulativeAverage', () => { })).toEqual([0, 0.5, 1, 1.5, 1.4]) }); - test ('returns cumulative average with zero values replacing invalid values', () => { + test('returns cumulative average with zero values replacing invalid values', () => { expect(getCumulativeAverage({ values: invalid, config: { convertInvalidToZero: true } @@ -3486,11 +3488,11 @@ describe('getCumulativeMedian', () => { const valid = [0, 1, 2, 3, 1]; const invalid = [0, 1, NaN, undefined, null, Infinity, -Infinity, 2, 3, 1]; test('returns cumulative median for a complete array of numbers', () => { - expect(getCumulativeMedian({values: valid})).toEqual([0, 0.5, 1, 1.5, 1]); + expect(getCumulativeMedian({ values: valid })).toEqual([0, 0.5, 1, 1.5, 1]); }); test('returns cumulative median and invalid values, but invalid values ignored in median', () => { - expect(getCumulativeMedian({values: invalid})).toEqual([0, 0.5, NaN, undefined, null, Infinity, -Infinity, 1, 1.5, 1]); + expect(getCumulativeMedian({ values: invalid })).toEqual([0, 0.5, NaN, undefined, null, Infinity, -Infinity, 1, 1.5, 1]); }); test('returns cumulative median without invalid values', () => { @@ -3500,10 +3502,160 @@ describe('getCumulativeMedian', () => { })).toEqual([0, 0.5, 1, 1.5, 1]) }); - test ('returns cumulative median with zero values replacing invalid values', () => { + test('returns cumulative median with zero values replacing invalid values', () => { expect(getCumulativeMedian({ values: invalid, config: { convertInvalidToZero: true } })).toEqual([0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0]) }) -}); \ No newline at end of file +}); + +describe('createTSpansFromLineBreaksOnX', () => { + test('creates a single for content without line breaks', () => { + const result = createTSpansFromLineBreaksOnX({ + content: 'Hello World', + fontSize: 10, + fill: 'red', + x: 0, + y: 0, + }); + const expected = `Hello World`; + expect(result).toBe(expected); + }); + + test('creates multiple elements split by newline', () => { + const result = createTSpansFromLineBreaksOnX({ + content: 'Line1\nLine2', + fontSize: 12, + fill: '#00f', + x: 5, + y: 10, + }); + const lineHeight = 12 * 1.3; + const expected = [ + `Line1`, + `Line2` + ].join(''); + expect(result).toBe(expected); + }); + + test('handles empty content as a single empty ', () => { + const result = createTSpansFromLineBreaksOnX({ + content: '', + fontSize: 8, + fill: 'black', + x: 1, + y: 2, + }); + const expected = ``; + expect(result).toBe(expected); + }); + + test('handles trailing newline producing an empty line', () => { + const result = createTSpansFromLineBreaksOnX({ + content: 'A\nB\n', + fontSize: 15, + fill: 'green', + x: 2, + y: 3, + }); + const lineHeight = 15 * 1.3; + const expected = [ + `A`, + `B`, + `` + ].join(''); + expect(result).toBe(expected); + }); + + test('handles multiple consecutive newlines correctly', () => { + const result = createTSpansFromLineBreaksOnX({ + content: 'X\n\nY', + fontSize: 5, + fill: 'blue', + x: 0, + y: 0, + }); + const lineHeight = 5 * 1.3; + const expected = [ + `X`, + ``, + `Y` + ].join(''); + expect(result).toBe(expected); + }); +}) + +describe('createTSpansFromLineBreaksOnY', () => { + test('single-line content produces one tspan with dy=0', () => { + const result = createTSpansFromLineBreaksOnY({ + content: 'Hello', + fontSize: 10, + fill: 'red', + x: 5, + }); + const expected = `Hello`; + expect(result).toBe(expected); + }); + + test('two-line content splits into two tspans with correct dy offsets', () => { + const fontSize = 12; + const result = createTSpansFromLineBreaksOnY({ + content: 'Line1\nLine2', + fontSize, + fill: '#00f', + x: 0, + }); + const dy = fontSize * 1.3; + const expected = [ + `Line1`, + `Line2`, + ].join(''); + expect(result).toBe(expected); + }); + + test('empty content yields a single empty tspan', () => { + const result = createTSpansFromLineBreaksOnY({ + content: '', + fontSize: 8, + fill: 'black', + x: 2, + }); + const expected = ``; + expect(result).toBe(expected); + }); + + test('trailing newline creates an extra empty tspan line', () => { + const fontSize = 15; + const result = createTSpansFromLineBreaksOnY({ + content: 'A\nB\n', + fontSize, + fill: 'green', + x: 1, + }); + const dy = fontSize * 1.3; + const expected = [ + `A`, + `B`, + `` + ].join(''); + expect(result).toBe(expected); + }); + + test('multiple consecutive newlines produce intermediate empty tspans', () => { + const fontSize = 5; + const result = createTSpansFromLineBreaksOnY({ + content: 'X\n\nY', + fontSize, + fill: 'blue', + x: 3, + }); + const dy = fontSize * 1.3; + const expected = [ + `X`, + ``, + `Y` + ].join(''); + expect(result).toBe(expected); + }); +}) \ No newline at end of file From b7a2466841439cefc114d9d75cb929f1f2484038 Mon Sep 17 00:00:00 2001 From: graphieros Date: Wed, 9 Jul 2025 14:41:44 +0200 Subject: [PATCH 2/8] New feature - Display mutliline labels when they include line breaks --- TestingArena/ArenaVueUiHistoryPlot.vue | 2 +- .../ArenaVueUiParallelCoordinatePlot.vue | 2 +- TestingArena/ArenaVueUiQuadrant.vue | 2 +- TestingArena/ArenaVueUiQuickChart.vue | 7 +- TestingArena/ArenaVueUiRidgeline.vue | 7 +- TestingArena/ArenaVueUiStackbar.vue | 11 +- TestingArena/ArenaVueUiStripPlot.vue | 4 +- TestingArena/ArenaVueUiTreemap.vue | 2 +- TestingArena/ArenaVueUiVerticalBar.vue | 2 +- TestingArena/ArenaVueUiWaffle.vue | 6 +- TestingArena/ArenaVueUiXy.vue | 7 +- src/components/vue-ui-donut-evolution.vue | 42 ++++-- src/components/vue-ui-history-plot.vue | 64 ++++++--- .../vue-ui-parallel-coordinate-plot.vue | 22 ++++ src/components/vue-ui-quadrant.vue | 44 +++++-- src/components/vue-ui-quick-chart.vue | 38 ++++-- src/components/vue-ui-ridgeline.vue | 29 ++++- src/components/vue-ui-stackbar.vue | 123 +++++++++++++----- src/components/vue-ui-strip-plot.vue | 44 +++++-- src/components/vue-ui-xy.vue | 25 ++++ 20 files changed, 366 insertions(+), 117 deletions(-) diff --git a/TestingArena/ArenaVueUiHistoryPlot.vue b/TestingArena/ArenaVueUiHistoryPlot.vue index 8fbfa6ae..7b878856 100644 --- a/TestingArena/ArenaVueUiHistoryPlot.vue +++ b/TestingArena/ArenaVueUiHistoryPlot.vue @@ -12,7 +12,7 @@ const dataset = ref([ { name: 'Series 1', values: [ - { x: 2, y: 0, label: 'T1'}, + { x: 2, y: 0, label: 'T1 with some sort\nof long name'}, { x: 18, y: 0.2, label: 'T2'}, { x: 21, y: 0.8, label: 'T3'}, { x: 19, y: 1.2, label: 'T4'}, diff --git a/TestingArena/ArenaVueUiParallelCoordinatePlot.vue b/TestingArena/ArenaVueUiParallelCoordinatePlot.vue index b5de81ed..3a98dc00 100644 --- a/TestingArena/ArenaVueUiParallelCoordinatePlot.vue +++ b/TestingArena/ArenaVueUiParallelCoordinatePlot.vue @@ -166,7 +166,7 @@ const config = computed(() => { ...c.style.chart.yAxis.labels, roundings: [0, 0, 0, 1], suffixes: ['$', '€', '£', '%'], - axisNames: ['Axis 1', 'Axis 2', 'Axis 3', ''], + axisNames: ['Axis 1 with some\nsort of long name', 'Axis 2', 'Axis 3', ''], formatters: [ ({value, config}) => { return `f0 | ${value}` diff --git a/TestingArena/ArenaVueUiQuadrant.vue b/TestingArena/ArenaVueUiQuadrant.vue index d4206d19..2931067f 100644 --- a/TestingArena/ArenaVueUiQuadrant.vue +++ b/TestingArena/ArenaVueUiQuadrant.vue @@ -12,7 +12,7 @@ function makeDs(n,m) { const arr = []; for(let i = 0; i < n; i += 1) { arr.push({ - name: 'Serie', + name: 'Serie with a name\n that is way too long', x: Math.random() > 0.5 ? Math.random()*m : -Math.random()*m, y: Math.random() > 0.5 ? Math.random()*m : -Math.random()*m, }) diff --git a/TestingArena/ArenaVueUiQuickChart.vue b/TestingArena/ArenaVueUiQuickChart.vue index 83d343d9..80513947 100644 --- a/TestingArena/ArenaVueUiQuickChart.vue +++ b/TestingArena/ArenaVueUiQuickChart.vue @@ -262,9 +262,12 @@ const config = computed(() => { }, theme: currentTheme.value, customPalette: ['#6376DD', "#DD3322", "#66DDAA"], - xyPeriods: monthValues.value, + // xyPeriods: monthValues.value, + xyPeriods: new Array(100).fill(0).map((el,i) => { + return `Some long label\nfor index ${i}` + }), datetimeFormatter: { - enable: true + enable: false } } }) diff --git a/TestingArena/ArenaVueUiRidgeline.vue b/TestingArena/ArenaVueUiRidgeline.vue index eb6555a6..8a36b428 100644 --- a/TestingArena/ArenaVueUiRidgeline.vue +++ b/TestingArena/ArenaVueUiRidgeline.vue @@ -169,9 +169,12 @@ const config = computed(() => { ...c.style.chart.xAxis, labels: { ...c.style.chart.xAxis.labels, - values: monthValues.value, + // values: monthValues.value, + values: new Array(12).fill(0).map((el,i) => { + return `Some long label\n with index ${i}` + }), datetimeFormatter: { - enable: true, + enable: false, } } } diff --git a/TestingArena/ArenaVueUiStackbar.vue b/TestingArena/ArenaVueUiStackbar.vue index 38f98b3b..40a63d90 100644 --- a/TestingArena/ArenaVueUiStackbar.vue +++ b/TestingArena/ArenaVueUiStackbar.vue @@ -253,9 +253,12 @@ const config = computed(() => { ...c.style.chart.grid.x, timeLabels: { ...c.style.chart.grid.x.timeLabels, - values: monthValues.value, + // values: monthValues.value, + values: new Array(6).fill(0).map((d, i) => { + return `Some long name\nfor dataset of index ${i}` + }), datetimeFormatter: { - enable: true + enable: false } // values: ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG'] } @@ -337,7 +340,7 @@ function selectTimeLabel(data) { diff --git a/src/components/vue-ui-ridgeline.vue b/src/components/vue-ui-ridgeline.vue index d3694239..9340a7d3 100644 --- a/src/components/vue-ui-ridgeline.vue +++ b/src/components/vue-ui-ridgeline.vue @@ -9,6 +9,7 @@ import { createSmoothAreaSegments, createSmoothPath, createStraightPath, + createTSpansFromLineBreaksOnX, createUid, dataLabel, deepEmptyObjectToNull, @@ -861,11 +862,14 @@ defineExpose({ x: xLabel.selectorX, y: drawableArea.top + xLabel.height + FINAL_CONFIG.style.chart.xAxis.labels.offsetY }"> - + + {{ xLabel.label }} + + + + diff --git a/src/components/vue-ui-stackbar.vue b/src/components/vue-ui-stackbar.vue index f73b0d07..cfbef315 100755 --- a/src/components/vue-ui-stackbar.vue +++ b/src/components/vue-ui-stackbar.vue @@ -8,8 +8,10 @@ import { calculateNiceScaleWithExactExtremes, convertColorToHex, convertCustomPalette, - createCsvContent, - createUid, + createCsvContent, + createTSpansFromLineBreaksOnX, + createTSpansFromLineBreaksOnY, + createUid, dataLabel, downloadCsv, error, @@ -1347,21 +1349,55 @@ defineExpose({ - - {{ timeLabel.text }} - + + + + {{ timeLabel.text }} + + + @@ -1383,22 +1419,43 @@ defineExpose({ - - {{ timeLabel.text }} - + + + {{ timeLabel.text }} + + + diff --git a/src/components/vue-ui-strip-plot.vue b/src/components/vue-ui-strip-plot.vue index 110fb1d0..b9fd4205 100644 --- a/src/components/vue-ui-strip-plot.vue +++ b/src/components/vue-ui-strip-plot.vue @@ -7,6 +7,7 @@ import { convertColorToHex, convertCustomPalette, createCsvContent, + createTSpansFromLineBreaksOnX, createUid, darkenHexColor, dataLabel, @@ -728,17 +729,38 @@ defineExpose({ diff --git a/src/components/vue-ui-xy.vue b/src/components/vue-ui-xy.vue index 67581a56..0d7eabd1 100644 --- a/src/components/vue-ui-xy.vue +++ b/src/components/vue-ui-xy.vue @@ -1170,7 +1170,9 @@ @@ -1672,6 +1695,7 @@ import { createStraightPath, createStar, createTSpans, + createTSpansFromLineBreaksOnX, createUid, dataLabel, downloadCsv, @@ -3029,6 +3053,7 @@ export default { createSmoothPath, createStraightPath, createTSpans, + createTSpansFromLineBreaksOnX, dataLabel, downloadCsv, error, From 4cf4522493d75b81dc34c4754b8bfefd5e00122c Mon Sep 17 00:00:00 2001 From: graphieros Date: Wed, 9 Jul 2025 14:42:22 +0200 Subject: [PATCH 3/8] Fix - VueUiDonut - Remove opacity from arc colors --- src/components/vue-ui-donut.vue | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/vue-ui-donut.vue b/src/components/vue-ui-donut.vue index 7350741e..c6fe6a76 100644 --- a/src/components/vue-ui-donut.vue +++ b/src/components/vue-ui-donut.vue @@ -973,6 +973,12 @@ defineExpose({