Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
- `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 shows every variable its query selects, or every property its graph's resources carry, instead of only its category and series
- 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ exclude-result-prefixes="#all"
<xsl:with-param name="chart-type" select="$chart-type"/>
<xsl:with-param name="category" select="$category"/>
<xsl:with-param name="series" select="$series"/>
<xsl:with-param name="height" select="400"/>
<!-- a table is as tall as its rows. A bar chart draws its categories down the side and skips every other
label once they no longer fit, so it grows by a bar's height for each row past what 400px holds -->
<xsl:with-param name="height" select="if ($chart-type = '&ac;Table') then () else if ($chart-type = '&ac;BarChart') then max((400, 120 + 30 * xs:integer(ixsl:call($data-table, 'getNumberOfRows', [])))) else 400"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
Expand Down
97 changes: 97 additions & 0 deletions tests/ui/specs/document/blocks/chart/size.spec.mjs
Original file line number Diff line number Diff line change
@@ -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: <http://rdfs.org/sioc/ns#>
PREFIX dct: <http://purl.org/dc/terms/>

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: <http://rdfs.org/sioc/ns#>
PREFIX dct: <http://purl.org/dc/terms/>

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 });
}
});
});
Loading