From 9360202b09980679529a197d2dfa7912d578fa0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 29 Oct 2026 18:00:02 +0100 Subject: [PATCH] A Table chart is as tall as its rows, and a bar chart grows with its bars Every chart was drawn 400px tall: by .chart-canvas in ldh.css and again by the height ldh:RenderChart handed ac:draw-chart, whatever the chart type. A Table of three rows stretched them to fill 400px and cut a long one off. A bar chart draws its categories down the side, and once they no longer fit Google Charts skips every other label. ldh:RenderChart passes no height for a Table, and for a bar chart max(400, 120 + 30 per row); the other charts keep 400. The canvas holds 400px as a min-height while it loads instead of a fixed height, so the drawing sets it, and a canvas holding a table has none. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../com/atomgraph/linkeddatahub/css/ldh.css | 7 +- .../linkeddatahub/xsl/client/block/chart.xsl | 4 +- .../specs/document/blocks/chart/size.spec.mjs | 97 +++++++++++++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 tests/ui/specs/document/blocks/chart/size.spec.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index d04a70266..b37e9b6f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ - A write whose `Accept` names no RDF syntax is no longer refused with `406` - `405` responses list the allowed methods, a `415` to `PATCH` names the accepted type, and access requests answer `201` - An anonymous `HEAD` on a document the requester may only append to is answered `200` with its entity tag instead of `403`: the front Varnish fetched a HEAD as a GET on a cache miss, so the platform decided it as a read. `HEAD` now bypasses the cache and is decided as a HEAD, the way a conditional write by an agent without `acl:Read` needs +- A chart drawn as a Table is as tall as its rows instead of 400px, and a bar chart grows with its bars so that every bar keeps its label ### Security - A document's types, which authorizations match on, come from its own graph only, so a write to one document cannot change another's access diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/ldh.css b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/ldh.css index d35995bfb..84a218e93 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/ldh.css +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/ldh.css @@ -117,9 +117,12 @@ div[typeof="https://w3id.org/atomgraph/linkeddatahub#XHTML"] .ldh-edit-form .ldh div[typeof="https://w3id.org/atomgraph/linkeddatahub#XHTML"] .ldh-edit-form .ldh-prop-form, div[typeof="https://w3id.org/atomgraph/linkeddatahub#XHTML"] .ldh-edit-form .ldh-prop-group, div[typeof="https://w3id.org/atomgraph/linkeddatahub#XHTML"] .ldh-edit-form .ldh-prop-row, div[typeof="https://w3id.org/atomgraph/linkeddatahub#XHTML"] .ldh-edit-form .ldh-prop-row > .value, div[typeof="https://w3id.org/atomgraph/linkeddatahub#XHTML"] .ldh-edit-form .val-main { display: block; padding-left: 0; padding-right: 0; } div[typeof="https://w3id.org/atomgraph/linkeddatahub#XHTML"] form fieldset { border: none; } object { display: block; margin: auto; max-height: 400px; overflow: hidden; width: 100%; } -.chart-canvas { width: 100%; height: 400px; } +/* the 400px a chart is drawn at, held while it loads; the drawing sets the height, so a bar chart of many rows is taller */ +.chart-canvas { width: 100%; min-height: 400px; } /* an empty result set replaces the plot with the block empty state, which would otherwise sit stranded above 400px of nothing; its own padding supplies the inset the canvas would add */ -.chart-canvas:has(> .ldh-block-blank) { height: auto; padding: 0; } +.chart-canvas:has(> .ldh-block-blank) { min-height: 0; padding: 0; } +/* a table is as tall as its rows */ +.chart-canvas:has(.google-visualization-table) { min-height: 0; } /* Google Charts Table specific (the ac:Table chart type renders as HTML, skinned here rather than via draw options) */ .chart-canvas .google-visualization-table-table { font-family: var(--font-sans); font-size: var(--fs-sm); color: var(--fg-1); } .chart-canvas .google-visualization-table-table th { background: var(--surface-1); color: var(--fg-muted); font-weight: var(--fw-semibold); border-color: var(--border-default); } diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/block/chart.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/block/chart.xsl index 8107a9fe5..bb83e076f 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/block/chart.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/block/chart.xsl @@ -206,7 +206,9 @@ exclude-result-prefixes="#all" - + + diff --git a/tests/ui/specs/document/blocks/chart/size.spec.mjs b/tests/ui/specs/document/blocks/chart/size.spec.mjs new file mode 100644 index 000000000..2d25e8624 --- /dev/null +++ b/tests/ui/specs/document/blocks/chart/size.spec.mjs @@ -0,0 +1,97 @@ +// A chart is as tall as what it draws. +// +// Every chart was drawn 400px tall, by the canvas's CSS and again by the height ldh:RenderChart handed Google Charts, +// whatever the chart type. A Table of three rows stretched them to fill 400px and cut a long one off. A bar chart +// draws its categories down the side, and once they no longer fit in 400px Google Charts skips every other label, so +// a bar chart of 25 items named 13 of them. +// +// Each test seeds a document of its own: the shared fixtures chart three kinds, which fit either way. +import { test, expect } from '../../../../lib/console.mjs'; +import { goto } from '../../../../lib/settle.mjs'; +import { fixtures, ldh, itemNumbers, itemTitle } from '../../../../lib/fixtures.mjs'; +import { endUserBase } from '../../../../lib/stack.mjs'; +import { canvas, chartBlock, drawing } from '../../../../lib/chart.mjs'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const AC = 'https://w3id.org/atomgraph/client#'; + +// One row per kind: three +const kindsQuery = `PREFIX sioc: +PREFIX dct: + +SELECT ?kind (COUNT(?item) AS ?items) +WHERE + { GRAPH ?g + { ?item sioc:has_container <${fixtures.container}> ; + dct:description ?kind + } + } +GROUP BY ?kind +ORDER BY ?kind`; + +// One row per item, named by its number alone, so that no label is long enough to be cut short +const itemsQuery = `PREFIX sioc: +PREFIX dct: + +SELECT ?number (STRLEN(?title) AS ?chars) +WHERE + { GRAPH ?g + { ?item sioc:has_container <${fixtures.container}> ; + dct:title ?title + } + BIND (STRAFTER(?title, "item ") AS ?number) + } +ORDER BY ?number`; + +const queryFile = text => { + const file = join(mkdtempSync(join(tmpdir(), 'ui-chart-size-')), 'q.rq'); + writeFileSync(file, text); + return file; +}; + +async function seedChart(slug, query, type, category, series) { + const doc = `${endUserBase}${slug}/`; + await ldh(['delete', doc], { allowFailure: true }); + await ldh(['create', 'item', '--container', endUserBase, '--title', `Chart size ${slug}`, '--slug', slug]); + await ldh(['add', 'select', '--title', 'Query', '--uri', `${doc}#query`, '--query-file', queryFile(query), doc]); + await ldh(['add', 'result-set-chart', '--title', 'Chart', '--uri', `${doc}#chart`, '--query', `${doc}#query`, + '--chart-type', type, '--category-var-name', category, '--series-var-name', series, doc]); + await ldh(['add', 'object-block', '--title', 'Chart block', '--uri', `${doc}#block`, '--value', `${doc}#chart`, doc]); + return { doc, chart: `${doc}#chart` }; +} + +// the documents are the owner's, unpublished, so only the owner can read them +test.describe('the size of a chart', { tag: '@owner' }, () => { + test('a table is as tall as its rows', async ({ page }) => { + const { doc, chart } = await seedChart('ui-chart-size-table', kindsQuery, `${AC}Table`, 'kind', 'items'); + try { + await goto(page, doc); + const table = canvas(chartBlock(page, chart)).locator('.google-visualization-table-table'); + await expect(table.locator('tr')).toHaveCount(4, { timeout: 30_000 }); + + // a header and three rows, drawn at the height of a header and three rows - not stretched to 400px + const rows = await table.locator('tr').evaluateAll(trs => trs.map(tr => tr.getBoundingClientRect().height)); + expect(Math.max(...rows), 'a row was stretched').toBeLessThan(80); + expect((await canvas(chartBlock(page, chart)).boundingBox()).height).toBeLessThan(400); + } finally { + await ldh(['delete', doc], { allowFailure: true }); + } + }); + + test('a bar chart names every bar', async ({ page }) => { + const { doc, chart } = await seedChart('ui-chart-size-bars', itemsQuery, `${AC}BarChart`, 'number', 'chars'); + try { + await goto(page, doc); + const block = chartBlock(page, chart); + await expect(drawing(block)).toBeVisible({ timeout: 30_000 }); + + const numbers = itemNumbers().map(n => itemTitle(n).split(' ').pop()); + const labels = await canvas(block).locator('svg text').allTextContents(); + expect(numbers.filter(number => !labels.includes(number)), 'bars without a label').toEqual([]); + } finally { + await ldh(['delete', doc], { allowFailure: true }); + } + }); +});