Skip to content

Commit 4c2e259

Browse files
committed
change how SparklinesGrid.vue does its layouting, instead of using Materialized css, we can start using grid-css. This will allow us to have more dynamic views based on what we have (multiple dates to compare etc)
1 parent 15e257a commit 4c2e259

7 files changed

Lines changed: 136 additions & 128 deletions

File tree

plugins/CoreVisualizations/Visualizations/Sparklines.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ public function render()
133133

134134
/**
135135
* Whether the redesigned Vue card grid can render the current request. It handles the
136-
* no-comparison layout and date comparison of exactly two dates; segment comparison,
137-
* segment + date comparison, and comparing three or more dates stay on the legacy Twig layout.
136+
* no-comparison layout and date comparison of two or more dates; segment comparison and
137+
* segment + date comparison stay on the legacy Twig layout.
138138
*/
139139
private function isRedesignSupportedForComparison(): bool
140140
{
@@ -146,9 +146,12 @@ private function isRedesignSupportedForComparison(): bool
146146
$compareSegments = $request['compareSegments'] ?? [];
147147
$compareDates = $request['compareDates'] ?? [];
148148

149+
// A non-empty compareDates (one entry beyond the main date, i.e. two total) means a pure
150+
// date comparison. The upper bound is enforced upstream by DataComparisonFilter, which
151+
// rejects more than the configured periodCompareLimit before rendering.
149152
return empty($compareSegments)
150153
&& is_array($compareDates)
151-
&& count($compareDates) === 1;
154+
&& count($compareDates) >= 1;
152155
}
153156

154157
/**

plugins/CoreVisualizations/vue/dist/CoreVisualizations.umd.js

Lines changed: 27 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/CoreVisualizations/vue/dist/CoreVisualizations.umd.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/CoreVisualizations/vue/src/Sparklines/DateComparison.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,24 @@ describe('CoreVisualizations/DateComparison', () => {
7575
expect(wrapper.findAll('.dateComparison__period').length).toBe(2);
7676
});
7777

78+
it('renders a column for every compared date, including three or more', () => {
79+
const wrapper = createWrapper(makeSparkline({
80+
url: '?module=API&action=get&columns=nb_visits&compareDates[]=2026-05-03&compareDates[]=2026-05-02',
81+
metrics: {
82+
'Monday, May 4, 2026': [{ value: '10,558', description: 'Visits', title: 'Visits' }],
83+
'Sunday, May 3, 2026': [{ value: '12,558', description: 'Visits', title: 'Visits' }],
84+
'Saturday, May 2, 2026': [{ value: '9,213', description: 'Visits', title: 'Visits' }],
85+
},
86+
seriesIndices: [0, 1, 2],
87+
}));
88+
89+
const labels = wrapper.findAll('.dateAtom').map((node) => node.text());
90+
expect(labels).toEqual(['Monday, May 4, 2026', 'Sunday, May 3, 2026', 'Saturday, May 2, 2026']);
91+
expect(wrapper.findAll('.dateComparison__period').length).toBe(3);
92+
expect(wrapper.findAll('.metricValue__number').map((node) => node.text()))
93+
.toEqual(['10,558', '12,558', '9,213']);
94+
});
95+
7896
it('renders the primary and secondary value of each date column', () => {
7997
const wrapper = createWrapper();
8098

plugins/CoreVisualizations/vue/src/SparklinesGrid/SparklinesGrid.less

Lines changed: 22 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,70 +8,30 @@
88
// Scoped to .sparklinesGrid — Matomo concatenates all LESS into one sheet, so this avoids
99
// colliding with the legacy .sparkline styles still used when the feature flag is off.
1010
.sparklinesGrid {
11-
// Row gap: each column's bottom padding, cancelled on the last row by an equal negative
12-
// margin on the grid. Shared so the two stay in sync.
13-
@_row-gap: 16px;
14-
15-
// Columns come from Materialize's float classes (col s6 m6 l4 xl3 = 2/2/4/4); the media
16-
// queries below add a 5th tier. Floats keep an incomplete last row packed left (flex broke
17-
// that). The card frame itself lives in SparklineCard.less.
18-
.col {
19-
padding: 0 8px @_row-gap;
20-
}
21-
22-
// The grid is itself the .row; pull it up one gap to cancel the last row's bottom padding
23-
// (the same offset idiom Materialize uses for the horizontal .col padding).
24-
&.row {
25-
margin-bottom: -@_row-gap;
26-
}
27-
28-
// Comparison cards: 3-column tier from 1600px (xl6 is 2 cols up to there). 1600 isn't a native
29-
// Materialize breakpoint, so set the width directly. xl6 is emitted only for comparison cards, so
30-
// this never affects the no-comparison grid.
31-
@media (min-width: 1600px) {
32-
&.row > .col.xl6 {
33-
width: 33.3333%;
34-
}
35-
}
36-
37-
// Top tier above 1920px: no-comparison widens to 5 cols, comparison to 4. 5 isn't a native col
38-
// fraction (12/5), so set the width directly; the selectors need .xl3/.xl6 (4 classes) to outrank
39-
// Materialize's `.row .col.xlN`. Placed after the 1600 block so the comparison rule wins at ≥1920.
40-
@media (min-width: 1920px) {
41-
&.row > .col.xl3 {
42-
width: 20%;
43-
}
44-
45-
&.row > .col.xl6 {
46-
width: 25%;
47-
}
48-
}
49-
50-
// Single column below 320px, same direct-width override; .s6 outranks `.row .col.s6`.
51-
@media (max-width: 320px) {
52-
&.row > .col.s6 {
53-
width: 100%;
54-
}
11+
@_gap: 16px;
12+
13+
// Auto-fill CSS Grid: the column count follows the available width instead of fixed
14+
// breakpoints. `--sparklines-card-min-width` is set inline by SparklinesGrid.vue (larger for
15+
// comparison cards, and scaled by the number of compared dates), so wider cards pack fewer per
16+
// row. `auto-fill` (not `auto-fit`) keeps an incomplete last row packed left at track width,
17+
// preserving the old float behaviour. `min(…, 100%)` stops a card whose minimum is wider than
18+
// the container (narrow widget / mobile) from forcing horizontal overflow — it is escaped
19+
// (~"…") because less.php has a built-in min() that would otherwise fold it to 100% at compile
20+
// time, collapsing the grid to a single full-width column.
21+
display: grid;
22+
grid-template-columns: repeat(auto-fill, minmax(~"min(var(--sparklines-card-min-width, 260px), 100%)", 1fr));
23+
gap: @_gap;
24+
25+
// Grid items default to min-width:auto, which would refuse to shrink below their content;
26+
// let each card shrink to its track so long titles/sparklines truncate instead of overflowing.
27+
> .sparklineCard {
28+
min-width: 0;
5529
}
5630
}
5731

58-
// Widget overrides so the cards sit neatly inside a dashboard widget.
32+
// Widget frame so the cards sit neatly inside a dashboard widget: the widget container adds no
33+
// padding of its own, so the grid supplies an even 16px frame plus an 8px inter-card gutter.
5934
.widget .sparklinesGrid {
60-
.col {
61-
// Inter-card gutter: 4px on every side, so 8px shows between adjacent cards.
62-
padding: 4px;
63-
}
64-
65-
.sparkline.sparklineCard {
66-
margin-left: 0;
67-
}
68-
69-
// The widget container adds no padding of its own, so the grid supplies an even frame around
70-
// the cards: 16px of row padding plus each column's 4px gutter gives 20px on every side. The
71-
// base .sparklinesGrid.row negative bottom margin is reset (margin: 0) so it can't pull the
72-
// frame in and push the last row past the widget's bottom edge.
73-
&.row {
74-
margin: 0;
75-
padding: 16px;
76-
}
35+
gap: 8px;
36+
padding: 16px;
7737
}

plugins/CoreVisualizations/vue/src/SparklinesGrid/SparklinesGrid.spec.ts

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,23 @@ describe('CoreVisualizations/SparklinesGrid', () => {
6666
};
6767
}
6868

69+
function comparisonEntry3Dates(order = 1) {
70+
// Same as comparisonEntry() but comparing three dates, so the grid should widen the card.
71+
return {
72+
url: '?module=API&action=get&compareDates[]=2026-05-03&compareDates[]=2026-05-02',
73+
metrics: {
74+
'Monday, May 4, 2026': [{ value: '1', description: 'Visits', title: 'Visits' }],
75+
'Sunday, May 3, 2026': [{ value: '2', description: 'Visits', title: 'Visits' }],
76+
'Saturday, May 2, 2026': [{ value: '3', description: 'Visits', title: 'Visits' }],
77+
},
78+
order,
79+
title: null,
80+
group: '0',
81+
seriesIndices: [0, 1, 2],
82+
graphParams: null,
83+
};
84+
}
85+
6986
function placeholder(order: number) {
7087
// Mirrors Config::addPlaceholder(): empty url + no metrics, used only for legacy layout.
7188
return {
@@ -122,11 +139,16 @@ describe('CoreVisualizations/SparklinesGrid', () => {
122139
expect(wrapper.find('.metricValue__title').attributes('title')).toBe('The number of visits.');
123140
});
124141

125-
it('uses the responsive grid columns (s6 m6 l4 xl3) on reporting pages', () => {
142+
it('lays cards out on a CSS grid, not the Materialize float row', () => {
126143
const wrapper = createWrapper();
127-
const col = wrapper.find('.row.sparklinesGrid > div');
144+
const grid = wrapper.find('.sparklinesGrid');
128145

129-
expect(col.classes()).toEqual(expect.arrayContaining(['col', 's6', 'm6', 'l4', 'xl3']));
146+
// The `row` class must be gone: its clearfix ::after pseudo-element would otherwise become a
147+
// stray empty grid cell.
148+
expect(grid.classes()).not.toContain('row');
149+
// Cards are direct grid children now (no per-card col wrapper div).
150+
expect(wrapper.findAllComponents({ name: 'SparklineCard' }).length).toBe(3);
151+
expect(grid.attributes('style')).toContain('--sparklines-card-min-width: 260px');
130152
});
131153

132154
it('orders cards by backend `order`, not by numeric group-key iteration order', () => {
@@ -146,33 +168,32 @@ describe('CoreVisualizations/SparklinesGrid', () => {
146168
expect(titles).toEqual(['First', 'Second', 'Third']);
147169
});
148170

149-
it('collapses to two columns in widget mode', () => {
171+
it('uses a smaller card minimum in widget mode so two still fit across', () => {
150172
const wrapper = createWrapper({ isWidget: true });
151-
const col = wrapper.find('.row.sparklinesGrid > div');
173+
const grid = wrapper.find('.sparklinesGrid');
152174

153-
expect(col.classes()).toContain('s6');
154-
expect(col.classes()).not.toContain('xl3');
175+
expect(grid.attributes('style')).toContain('--sparklines-card-min-width: 160px');
155176
});
156177

157-
it('uses the wider comparison columns (s12 m12 l6 xl6) when comparing', () => {
178+
it('widens the card minimum when comparing and renders the comparison body', () => {
158179
const wrapper = createWrapper({ sparklines: { 0: [comparisonEntry()] }, isComparing: true });
159-
const col = wrapper.find('.row.sparklinesGrid > div');
180+
const grid = wrapper.find('.sparklinesGrid');
160181

161-
expect(col.classes()).toEqual(expect.arrayContaining(['col', 's12', 'm12', 'l6', 'xl6']));
162-
// ...and the comparison body is what renders inside.
182+
// 2 compared dates -> 64 + 150 * 2.
183+
expect(grid.attributes('style')).toContain('--sparklines-card-min-width: 364px');
163184
expect(wrapper.findComponent({ name: 'DateComparison' }).exists()).toBe(true);
164185
});
165186

166-
it('collapses comparison cards to a single column in widget mode', () => {
187+
it('scales the comparison card minimum with the number of compared dates', () => {
167188
const wrapper = createWrapper({
168-
sparklines: { 0: [comparisonEntry()] },
189+
sparklines: { 0: [comparisonEntry3Dates()] },
169190
isComparing: true,
170-
isWidget: true,
171191
});
172-
const col = wrapper.find('.row.sparklinesGrid > div');
192+
const grid = wrapper.find('.sparklinesGrid');
173193

174-
expect(col.classes()).toContain('s12');
175-
expect(col.classes()).not.toContain('xl6');
194+
// 3 compared dates -> 64 + 150 * 3, and one column per date renders inside the card.
195+
expect(grid.attributes('style')).toContain('--sparklines-card-min-width: 514px');
196+
expect(wrapper.findAll('.dateComparison__period').length).toBe(3);
176197
});
177198

178199
it('re-runs the sparkline click-to-evolution wiring after mount', async () => {

0 commit comments

Comments
 (0)