diff --git a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/bg-BG/shell.json b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/bg-BG/shell.json index 69f5084b240..9fb067fa7d7 100644 --- a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/bg-BG/shell.json +++ b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/bg-BG/shell.json @@ -122,6 +122,8 @@ "any": "всички", "yes": "да", "no": "не", + "true": "Да", + "false": "Не", "min": "мин", "max": "макс", "contains": "съдържа..." diff --git a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/en-US/shell.json b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/en-US/shell.json index 8b816c154c3..ce89989ea21 100644 --- a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/en-US/shell.json +++ b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/en-US/shell.json @@ -122,6 +122,8 @@ "any": "any", "yes": "yes", "no": "no", + "true": "Yes", + "false": "No", "min": "min", "max": "max", "contains": "contains..." diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/report-file/report.js.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/report-file/report.js.template index acb9156f10d..1829ff4f253 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/report-file/report.js.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/report-file/report.js.template @@ -205,16 +205,47 @@ document.addEventListener('alpine:init', () => { const category = this.reportColumns.find(c => c.agg === 'NONE') || this.reportColumns[0]; let series = this.reportColumns.filter(c => c.agg && c.agg !== 'NONE'); if (!series.length) series = this.reportColumns.filter(c => c !== category); - const labels = this.rows.map(r => this.axisLabel(category ? r[category.key] : '')); - return { - labels, - series: series.map(c => ({ name: c.key, data: this.rows.map(r => Number(r[c.key]) || 0) })), - legend: series.length > 1, - }; + const labels = this.rows.map(r => this.axisLabel(category ? r[category.key] : '', category)); + const data = series.map(c => ({ name: c.key, data: this.rows.map(r => Number(r[c.key]) || 0) })); + // A circular chart slices the FIRST measure by the dimension, so the dimension values are named + // by the legend alone - it stays on whatever the series count - and each legend entry carries + // the slice's share of the measure total. Pie and doughnut additionally suppress the in-slice + // data label: the chart library prints the raw measure there with a percent sign, so a 336-hour + // sum reads "336%" (codbex/harmonia#113, fixed after 2.14.2). The share belongs in the legend + // either way. + if (this.chartType === 'pie' || this.chartType === 'doughnut') { + return { labels: this.shareLabels(labels, data[0]), series: data, legend: true, dataLabels: false }; + } + if (this.chartType === 'polarArea') { + return { labels: this.shareLabels(labels, data[0]), series: data, legend: true }; + } + return { labels, series: data, legend: data.length > 1 }; }, - // month(date) buckets are YYYYMM integers (e.g. 202607) - render them as "2026-07". - axisLabel(v) { + // "Billable" -> "Billable - 62%": a slice label with its share of the measure total, so a circular + // chart's legend reads as a split. Left untouched when there is no positive total to share, and + // when the fetched rows are only a window on the result (the dashboard tile fetches a few) - a + // page-local share is not the report's, the same rule the table footer applies to its totals. + shareLabels(labels, measure) { + if (this.count > this.rows.length) return labels; + const values = (measure && measure.data) || []; + const total = values.reduce((sum, v) => sum + (v > 0 ? v : 0), 0); + if (!(total > 0)) return labels; + return labels.map((label, i) => { + const v = values[i]; + if (!(v > 0)) return label; + const pct = (v / total) * 100; + return label + ' - ' + (pct < 10 ? pct.toFixed(1) : String(Math.round(pct))) + '%'; + }); + }, + + // month(date) buckets are YYYYMM integers (e.g. 202607) - render them as "2026-07"; a boolean + // dimension reads as Yes/No, never as the raw true/false. + axisLabel(v, meta) { + if (typeof v === 'boolean' || (meta && meta.kind === 'boolean' && v != null && v !== '')) { + const yes = v === true || v === 'true' || v === 1; + return yes ? T('application-core:shell.report.true', 'Yes') : T('application-core:shell.report.false', 'No'); + } const n = Number(v); if (Number.isInteger(n) && n >= 190001 && n <= 999912 && (n % 100) >= 1 && (n % 100) <= 12) { return Math.floor(n / 100) + '-' + String(n % 100).padStart(2, '0'); diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/report/chart-page.js.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/report/chart-page.js.template index e66409a88c8..eaa74f3b2a6 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/report/chart-page.js.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/report/chart-page.js.template @@ -41,7 +41,30 @@ document.addEventListener('alpine:init', () => { #end #end ]; +#if($layoutType == "REPORT_PIE" || $layoutType == "REPORT_DOUGHNUT" || $layoutType == "REPORT_POLARAREA") + // A circular chart's slices ARE the label column's values, named by the legend alone, so each + // label carries its share of the first series' total. + const values = (series[0] && series[0].data) || []; + const total = values.reduce((sum, v) => sum + (v > 0 ? v : 0), 0); + const sliceLabels = total > 0 + ? labels.map((label, i) => { + const v = values[i]; + if (!(v > 0)) return label; + const pct = (v / total) * 100; + return label + ' - ' + (pct < 10 ? pct.toFixed(1) : String(Math.round(pct))) + '%'; + }) + : labels; +#if($layoutType == "REPORT_POLARAREA") + return { labels: sliceLabels, series, legend: true }; +#else + // dataLabels off: the chart library prints the raw value inside a pie/doughnut slice with a + // percent sign, so a 336-hour sum reads "336%" (codbex/harmonia#113, fixed after 2.14.2). The + // share is in the legend instead. + return { labels: sliceLabels, series, legend: true, dataLabels: false }; +#end +#else return { labels, series }; +#end }, // Print the chart: rasterize the rendered SVG to a PNG (Harmonia.chartToImage) into a minimal diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java index 0ebd2a20e0b..041dccd0301 100644 --- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java @@ -261,10 +261,13 @@ class IntentEngineIT extends IntegrationTest { actions: [approve, reject] reports: + # chart: doughnut renders the aggregated rows as a circular chart beside the table - + # its slices ARE the dimension values, so the page names them in the legend. - name: OrdersByCustomer source: Order dimensions: [customer] measures: ["count(*)", "sum(total)"] + chart: doughnut # month(field) buckets a date dimension into a sortable YYYYMM integer. The widget # turns the report into a dashboard KPI: one aggregate cell, the month pinned to now. - name: OrdersByMonth @@ -3605,6 +3608,16 @@ void report_file_stack_generates_typed_column_filters() { assertTrue(page.contains("pattern: '### ### ### ##0.00'"), "the page metadata should carry the money pattern for decimal columns"); assertTrue(page.contains("limit: 20"), "an ordinary report should page in twenties"); + // A circular chart is sliced by the dimension, so its values are named by the legend alone - + // on whatever the series count - and each legend entry carries the slice's share of the + // measure total. The in-slice data label stays off for pie/doughnut: the chart library prints + // the raw measure there with a percent sign, which turned a 336-hour sum into "336%". + assertTrue(page.contains("chartType: 'doughnut'"), "the chart report page should carry its chart type"); + assertTrue(page.contains("legend: true") && page.contains("dataLabels: false"), + "a doughnut should keep its legend and drop the raw-value-as-percentage slice label"); + assertTrue(page.contains("shareLabels(labels, data[0])"), "doughnut legend entries should carry the slice share"); + assertTrue(page.contains("shell.report.true"), "a boolean dimension should read Yes/No in the chart, not true/false"); + // A statement's rows ARE its structure, so its page fetches the whole statement rather than // splitting a balance sheet across pages. restAssuredExecutor.execute(() -> given().contentType("application/json")