Coverage Report

Created: 2026-06-30 11:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/sc/source/ui/inc/SparklineRenderer.hxx
Line
Count
Source
1
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
/*
3
 * This file is part of the LibreOffice project.
4
 *
5
 * This Source Code Form is subject to the terms of the Mozilla Public
6
 * License, v. 2.0. If a copy of the MPL was not distributed with this
7
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
 *
9
 */
10
11
#pragma once
12
13
#include <document.hxx>
14
15
#include <basegfx/polygon/b2dpolygon.hxx>
16
#include <basegfx/polygon/b2dpolygontools.hxx>
17
#include <basegfx/matrix/b2dhommatrix.hxx>
18
#include <vcl/rendercontext/AntialiasingFlags.hxx>
19
20
#include <Sparkline.hxx>
21
#include <SparklineGroup.hxx>
22
#include <SparklineAttributes.hxx>
23
24
namespace sc
25
{
26
/** Contains the marker polygon and the color of a marker */
27
struct SparklineMarker
28
{
29
    basegfx::B2DPolygon maPolygon;
30
    Color maColor;
31
};
32
33
/** Sparkline value and action that needs to me performed on the value */
34
struct SparklineValue
35
{
36
    enum class Action
37
    {
38
        None, // No action on the value
39
        Skip, // Skip the value
40
        Interpolate // Interpolate the value
41
    };
42
43
    double maValue;
44
    Action meAction;
45
46
    SparklineValue(double aValue, Action eAction)
47
0
        : maValue(aValue)
48
0
        , meAction(eAction)
49
0
    {
50
0
    }
51
};
52
53
/** Contains and manages the values of the sparkline.
54
 *
55
 * It automatically keeps track of the minimums and maximums, and
56
 * skips or interpolates the sparkline values if needed, depending on
57
 * the input. This is done so it is easier to handle the sparkline
58
 * values later on.
59
 */
60
class SparklineValues
61
{
62
private:
63
    double mfPreviousValue = 0.0;
64
    size_t mnPreviousIndex = std::numeric_limits<size_t>::max();
65
66
    std::vector<size_t> maToInterpolateIndex;
67
68
    std::vector<SparklineValue> maValueList;
69
70
public:
71
    size_t mnFirstIndex = std::numeric_limits<size_t>::max();
72
    size_t mnLastIndex = 0;
73
74
    double mfMinimum = std::numeric_limits<double>::max();
75
    double mfMaximum = std::numeric_limits<double>::lowest();
76
77
0
    std::vector<SparklineValue> const& getValuesList() const { return maValueList; }
78
79
    void add(double fValue, SparklineValue::Action eAction)
80
0
    {
81
0
        maValueList.emplace_back(fValue, eAction);
82
0
        size_t nCurrentIndex = maValueList.size() - 1;
83
84
0
        if (eAction == SparklineValue::Action::None)
85
0
        {
86
0
            mnLastIndex = nCurrentIndex;
87
88
0
            if (mnLastIndex < mnFirstIndex)
89
0
                mnFirstIndex = mnLastIndex;
90
91
0
            if (fValue < mfMinimum)
92
0
                mfMinimum = fValue;
93
94
0
            if (fValue > mfMaximum)
95
0
                mfMaximum = fValue;
96
97
0
            interpolatePastValues(fValue, nCurrentIndex);
98
99
0
            mnPreviousIndex = nCurrentIndex;
100
0
            mfPreviousValue = fValue;
101
0
        }
102
0
        else if (eAction == SparklineValue::Action::Interpolate)
103
0
        {
104
0
            maToInterpolateIndex.push_back(nCurrentIndex);
105
0
            maValueList.back().meAction = SparklineValue::Action::Skip;
106
0
        }
107
0
    }
108
109
    static constexpr double interpolate(double x1, double y1, double x2, double y2, double x)
110
0
    {
111
0
        return (y1 * (x2 - x) + y2 * (x - x1)) / (x2 - x1);
112
0
    }
113
114
    void interpolatePastValues(double nCurrentValue, size_t nCurrentIndex)
115
0
    {
116
0
        if (maToInterpolateIndex.empty())
117
0
            return;
118
119
0
        if (mnPreviousIndex == std::numeric_limits<size_t>::max())
120
0
        {
121
0
            for (size_t nIndex : maToInterpolateIndex)
122
0
            {
123
0
                auto& rValue = maValueList[nIndex];
124
0
                rValue.meAction = SparklineValue::Action::Skip;
125
0
            }
126
0
        }
127
0
        else
128
0
        {
129
0
            for (size_t nIndex : maToInterpolateIndex)
130
0
            {
131
0
                double fInterpolated = interpolate(mnPreviousIndex, mfPreviousValue, nCurrentIndex,
132
0
                                                   nCurrentValue, nIndex);
133
134
0
                auto& rValue = maValueList[nIndex];
135
0
                rValue.maValue = fInterpolated;
136
0
                rValue.meAction = SparklineValue::Action::None;
137
0
            }
138
0
        }
139
0
        maToInterpolateIndex.clear();
140
0
    }
141
142
    void convertToStacked()
143
0
    {
144
        // transform the data to 1, -1
145
0
        for (auto& rValue : maValueList)
146
0
        {
147
0
            if (rValue.maValue != 0.0)
148
0
            {
149
0
                double fNewValue = rValue.maValue > 0.0 ? 1.0 : -1.0;
150
151
0
                if (rValue.maValue == mfMinimum)
152
0
                    fNewValue -= 0.01;
153
154
0
                if (rValue.maValue == mfMaximum)
155
0
                    fNewValue += 0.01;
156
157
0
                rValue.maValue = fNewValue;
158
0
            }
159
0
        }
160
0
        mfMinimum = -1.01;
161
0
        mfMaximum = 1.01;
162
0
    }
163
164
0
    void reverse() { std::reverse(maValueList.begin(), maValueList.end()); }
165
};
166
167
/** Iterator to traverse the addresses in a range if the range is one dimensional.
168
 *
169
 * The direction to traverse is detected automatically or hasNext returns
170
 * false if it is not possible to detect.
171
 *
172
 */
173
class RangeTraverser
174
{
175
    enum class Direction
176
    {
177
        UNKNOWN,
178
        ROW,
179
        COLUMN
180
    };
181
182
    ScAddress m_aCurrent;
183
    ScRange m_aRange;
184
    Direction m_eDirection;
185
186
public:
187
    RangeTraverser(ScRange const& rRange)
188
0
        : m_aCurrent(ScAddress::INITIALIZE_INVALID)
189
0
        , m_aRange(rRange)
190
0
        , m_eDirection(Direction::UNKNOWN)
191
192
0
    {
193
0
    }
194
195
    ScAddress const& first()
196
0
    {
197
0
        m_aCurrent.SetInvalid();
198
199
0
        if (m_aRange.aStart.Row() == m_aRange.aEnd.Row())
200
0
        {
201
0
            m_eDirection = Direction::COLUMN;
202
0
            m_aCurrent = m_aRange.aStart;
203
0
        }
204
0
        else if (m_aRange.aStart.Col() == m_aRange.aEnd.Col())
205
0
        {
206
0
            m_eDirection = Direction::ROW;
207
0
            m_aCurrent = m_aRange.aStart;
208
0
        }
209
210
0
        return m_aCurrent;
211
0
    }
212
213
    bool hasNext()
214
0
    {
215
0
        if (m_eDirection == Direction::COLUMN)
216
0
            return m_aCurrent.Col() <= m_aRange.aEnd.Col();
217
0
        else if (m_eDirection == Direction::ROW)
218
0
            return m_aCurrent.Row() <= m_aRange.aEnd.Row();
219
0
        else
220
0
            return false;
221
0
    }
222
223
    void next()
224
0
    {
225
0
        if (hasNext())
226
0
        {
227
0
            if (m_eDirection == Direction::COLUMN)
228
0
                m_aCurrent.IncCol();
229
0
            else if (m_eDirection == Direction::ROW)
230
0
                m_aCurrent.IncRow();
231
0
        }
232
0
    }
233
};
234
235
/** Render a provided sparkline into the input rectangle */
236
class SparklineRenderer
237
{
238
private:
239
    ScDocument& mrDocument;
240
    tools::Long mnOneX;
241
    tools::Long mnOneY;
242
243
    double mfScaleX;
244
    double mfScaleY;
245
246
    void createMarker(std::vector<SparklineMarker>& rMarkers, double x, double y,
247
                      Color const& rColor)
248
0
    {
249
0
        auto& rMarker = rMarkers.emplace_back();
250
0
        const double nHalfSizeX = mnOneX * 2 * mfScaleX;
251
0
        const double nHalfSizeY = mnOneY * 2 * mfScaleY;
252
0
        basegfx::B2DRectangle aRectangle(std::round(x - nHalfSizeX), std::round(y - nHalfSizeY),
253
0
                                         std::round(x + nHalfSizeX), std::round(y + nHalfSizeY));
254
0
        rMarker.maPolygon = basegfx::utils::createPolygonFromRect(aRectangle);
255
0
        rMarker.maColor = rColor;
256
0
    }
257
258
    void drawLine(vcl::RenderContext& rRenderContext, tools::Rectangle const& rRectangle,
259
                  SparklineValues const& rSparklineValues,
260
                  sc::SparklineAttributes const& rAttributes)
261
0
    {
262
0
        double nMax = rSparklineValues.mfMaximum;
263
0
        if (rAttributes.getMaxAxisType() == sc::AxisType::Custom && rAttributes.getManualMax())
264
0
            nMax = *rAttributes.getManualMax();
265
266
0
        double nMin = rSparklineValues.mfMinimum;
267
0
        if (rAttributes.getMinAxisType() == sc::AxisType::Custom && rAttributes.getManualMin())
268
0
            nMin = *rAttributes.getManualMin();
269
270
0
        std::vector<SparklineValue> const& rValueList = rSparklineValues.getValuesList();
271
0
        std::vector<basegfx::B2DPolygon> aPolygons;
272
0
        aPolygons.emplace_back();
273
0
        double numebrOfSteps = rValueList.size() - 1;
274
0
        double xStep = 0;
275
0
        double nDelta = nMax - nMin;
276
277
0
        std::vector<SparklineMarker> aMarkers;
278
0
        size_t nValueIndex = 0;
279
280
0
        for (auto const& rSparklineValue : rValueList)
281
0
        {
282
0
            if (rSparklineValue.meAction == SparklineValue::Action::Skip)
283
0
            {
284
0
                aPolygons.emplace_back();
285
0
            }
286
0
            else
287
0
            {
288
0
                auto& aPolygon = aPolygons.back();
289
0
                double nValue = rSparklineValue.maValue;
290
291
0
                double nP = (nValue - nMin) / nDelta;
292
0
                double x = rRectangle.GetWidth() * (xStep / numebrOfSteps);
293
0
                double y = rRectangle.GetHeight() - rRectangle.GetHeight() * nP;
294
295
0
                aPolygon.append({ x, y });
296
297
0
                if (rAttributes.isFirst() && nValueIndex == rSparklineValues.mnFirstIndex)
298
0
                {
299
0
                    createMarker(aMarkers, x, y, rAttributes.getColorFirst().getFinalColor());
300
0
                }
301
0
                else if (rAttributes.isLast() && nValueIndex == rSparklineValues.mnLastIndex)
302
0
                {
303
0
                    createMarker(aMarkers, x, y, rAttributes.getColorLast().getFinalColor());
304
0
                }
305
0
                else if (rAttributes.isHigh() && nValue == rSparklineValues.mfMaximum)
306
0
                {
307
0
                    createMarker(aMarkers, x, y, rAttributes.getColorHigh().getFinalColor());
308
0
                }
309
0
                else if (rAttributes.isLow() && nValue == rSparklineValues.mfMinimum)
310
0
                {
311
0
                    createMarker(aMarkers, x, y, rAttributes.getColorLow().getFinalColor());
312
0
                }
313
0
                else if (rAttributes.isNegative() && nValue < 0.0)
314
0
                {
315
0
                    createMarker(aMarkers, x, y, rAttributes.getColorNegative().getFinalColor());
316
0
                }
317
0
                else if (rAttributes.isMarkers())
318
0
                {
319
0
                    createMarker(aMarkers, x, y, rAttributes.getColorMarkers().getFinalColor());
320
0
                }
321
0
            }
322
323
0
            xStep++;
324
0
            nValueIndex++;
325
0
        }
326
327
0
        basegfx::B2DHomMatrix aMatrix;
328
0
        aMatrix.translate(rRectangle.Left(), rRectangle.Top());
329
330
0
        if (rAttributes.shouldDisplayXAxis())
331
0
        {
332
0
            double nZero = 0 - nMin / nDelta;
333
334
0
            if (nZero >= 0) // if nZero < 0, the axis is not visible
335
0
            {
336
0
                double x1 = 0.0;
337
0
                double x2 = double(rRectangle.GetWidth());
338
0
                double y = rRectangle.GetHeight() - rRectangle.GetHeight() * nZero;
339
340
0
                basegfx::B2DPolygon aAxisPolygon;
341
0
                aAxisPolygon.append({ x1, y });
342
0
                aAxisPolygon.append({ x2, y });
343
344
0
                rRenderContext.SetLineColor(rAttributes.getColorAxis().getFinalColor());
345
0
                rRenderContext.DrawPolyLineDirect(aMatrix, aAxisPolygon, 0.2 * mfScaleX);
346
0
            }
347
0
        }
348
349
0
        rRenderContext.SetLineColor(rAttributes.getColorSeries().getFinalColor());
350
351
0
        for (auto& rPolygon : aPolygons)
352
0
        {
353
0
            rRenderContext.DrawPolyLineDirect(aMatrix, rPolygon,
354
0
                                              rAttributes.getLineWeight() * mfScaleX, 0.0, nullptr,
355
0
                                              basegfx::B2DLineJoin::Round);
356
0
        }
357
358
0
        for (auto& rMarker : aMarkers)
359
0
        {
360
0
            rRenderContext.SetLineColor(rMarker.maColor);
361
0
            rRenderContext.SetFillColor(rMarker.maColor);
362
0
            auto& rPolygon = rMarker.maPolygon;
363
0
            rPolygon.transform(aMatrix);
364
0
            rRenderContext.DrawPolygon(rPolygon);
365
0
        }
366
0
    }
367
368
    static void setFillAndLineColor(vcl::RenderContext& rRenderContext,
369
                                    sc::SparklineAttributes const& rAttributes, double nValue,
370
                                    size_t nValueIndex, SparklineValues const& rSparklineValues)
371
0
    {
372
0
        if (rAttributes.isFirst() && nValueIndex == rSparklineValues.mnFirstIndex)
373
0
        {
374
0
            rRenderContext.SetLineColor(rAttributes.getColorFirst().getFinalColor());
375
0
            rRenderContext.SetFillColor(rAttributes.getColorFirst().getFinalColor());
376
0
        }
377
0
        else if (rAttributes.isLast() && nValueIndex == rSparklineValues.mnLastIndex)
378
0
        {
379
0
            rRenderContext.SetLineColor(rAttributes.getColorLast().getFinalColor());
380
0
            rRenderContext.SetFillColor(rAttributes.getColorLast().getFinalColor());
381
0
        }
382
0
        else if (rAttributes.isHigh() && nValue == rSparklineValues.mfMaximum)
383
0
        {
384
0
            rRenderContext.SetLineColor(rAttributes.getColorHigh().getFinalColor());
385
0
            rRenderContext.SetFillColor(rAttributes.getColorHigh().getFinalColor());
386
0
        }
387
0
        else if (rAttributes.isLow() && nValue == rSparklineValues.mfMinimum)
388
0
        {
389
0
            rRenderContext.SetLineColor(rAttributes.getColorLow().getFinalColor());
390
0
            rRenderContext.SetFillColor(rAttributes.getColorLow().getFinalColor());
391
0
        }
392
0
        else if (rAttributes.isNegative() && nValue < 0.0)
393
0
        {
394
0
            rRenderContext.SetLineColor(rAttributes.getColorNegative().getFinalColor());
395
0
            rRenderContext.SetFillColor(rAttributes.getColorNegative().getFinalColor());
396
0
        }
397
0
        else
398
0
        {
399
0
            rRenderContext.SetLineColor(rAttributes.getColorSeries().getFinalColor());
400
0
            rRenderContext.SetFillColor(rAttributes.getColorSeries().getFinalColor());
401
0
        }
402
0
    }
403
404
    void drawColumn(vcl::RenderContext& rRenderContext, tools::Rectangle const& rRectangle,
405
                    SparklineValues const& rSparklineValues,
406
                    sc::SparklineAttributes const& rAttributes)
407
0
    {
408
0
        double nMax = rSparklineValues.mfMaximum;
409
0
        if (rAttributes.getMaxAxisType() == sc::AxisType::Custom && rAttributes.getManualMax())
410
0
            nMax = *rAttributes.getManualMax();
411
412
0
        double nMin = rSparklineValues.mfMinimum;
413
0
        if (rAttributes.getMinAxisType() == sc::AxisType::Custom && rAttributes.getManualMin())
414
0
            nMin = *rAttributes.getManualMin();
415
416
0
        std::vector<SparklineValue> const& rValueList = rSparklineValues.getValuesList();
417
418
0
        basegfx::B2DPolygon aPolygon;
419
0
        basegfx::B2DHomMatrix aMatrix;
420
0
        aMatrix.translate(rRectangle.Left(), rRectangle.Top());
421
422
0
        double xStep = 0;
423
0
        double numberOfSteps = rValueList.size();
424
0
        double nDelta = nMax - nMin;
425
426
0
        double nColumnSize = rRectangle.GetWidth() / numberOfSteps;
427
0
        nColumnSize = nColumnSize - (nColumnSize * 0.3);
428
429
0
        double nZero = (0 - nMin) / nDelta;
430
0
        double nZeroPosition = 0.0;
431
0
        if (nZero >= 0)
432
0
        {
433
0
            nZeroPosition = rRectangle.GetHeight() - rRectangle.GetHeight() * nZero;
434
435
0
            if (rAttributes.shouldDisplayXAxis())
436
0
            {
437
0
                double x1 = 0.0;
438
0
                double x2 = double(rRectangle.GetWidth());
439
440
0
                basegfx::B2DPolygon aAxisPolygon;
441
0
                aAxisPolygon.append({ x1, nZeroPosition });
442
0
                aAxisPolygon.append({ x2, nZeroPosition });
443
444
0
                rRenderContext.SetLineColor(rAttributes.getColorAxis().getFinalColor());
445
0
                rRenderContext.DrawPolyLineDirect(aMatrix, aAxisPolygon, 0.2 * mfScaleX);
446
0
            }
447
0
        }
448
0
        else
449
0
            nZeroPosition = rRectangle.GetHeight();
450
451
0
        size_t nValueIndex = 0;
452
453
0
        for (auto const& rSparklineValue : rValueList)
454
0
        {
455
0
            double nValue = rSparklineValue.maValue;
456
457
0
            if (nValue != 0.0)
458
0
            {
459
0
                setFillAndLineColor(rRenderContext, rAttributes, nValue, nValueIndex,
460
0
                                    rSparklineValues);
461
462
0
                double nP = (nValue - nMin) / nDelta;
463
0
                double x = rRectangle.GetWidth() * (xStep / numberOfSteps);
464
0
                double y = rRectangle.GetHeight() - rRectangle.GetHeight() * nP;
465
466
0
                basegfx::B2DRectangle aRectangle(x, y, x + nColumnSize, nZeroPosition);
467
0
                aPolygon = basegfx::utils::createPolygonFromRect(aRectangle);
468
469
0
                aPolygon.transform(aMatrix);
470
0
                rRenderContext.DrawPolygon(aPolygon);
471
0
            }
472
0
            xStep++;
473
0
            nValueIndex++;
474
0
        }
475
0
    }
476
477
    bool isCellHidden(ScAddress const& rAddress)
478
0
    {
479
0
        return mrDocument.RowHidden(rAddress.Row(), rAddress.Tab())
480
0
               || mrDocument.ColHidden(rAddress.Col(), rAddress.Tab());
481
0
    }
482
483
public:
484
    SparklineRenderer(ScDocument& rDocument)
485
0
        : mrDocument(rDocument)
486
0
        , mnOneX(1)
487
0
        , mnOneY(1)
488
0
        , mfScaleX(1.0)
489
0
        , mfScaleY(1.0)
490
0
    {
491
0
    }
492
493
    void render(std::shared_ptr<sc::Sparkline> const& pSparkline,
494
                vcl::RenderContext& rRenderContext, tools::Rectangle const& rRectangle,
495
                tools::Long nOneX, tools::Long nOneY, double fScaleX, double fScaleY)
496
0
    {
497
0
        auto popIt = rRenderContext.ScopedPush();
498
499
0
        rRenderContext.SetAntialiasing(AntialiasingFlags::Enable);
500
0
        rRenderContext.SetClipRegion(vcl::Region(rRectangle));
501
502
0
        tools::Rectangle aOutputRectangle(rRectangle);
503
0
        aOutputRectangle.shrink(6); // provide border
504
505
0
        mnOneX = nOneX;
506
0
        mnOneY = nOneY;
507
0
        mfScaleX = fScaleX;
508
0
        mfScaleY = fScaleY;
509
510
0
        auto const& rRangeList = pSparkline->getInputRange();
511
512
0
        if (rRangeList.empty())
513
0
        {
514
0
            return;
515
0
        }
516
517
0
        auto pSparklineGroup = pSparkline->getSparklineGroup();
518
0
        auto const& rAttributes = pSparklineGroup->getAttributes();
519
520
0
        ScRange aRange = rRangeList[0];
521
522
0
        SparklineValues aSparklineValues;
523
524
0
        RangeTraverser aTraverser(aRange);
525
0
        for (ScAddress const& rCurrent = aTraverser.first(); aTraverser.hasNext();
526
0
             aTraverser.next())
527
0
        {
528
            // Skip if the cell is hidden and "displayHidden" attribute is not selected
529
0
            if (!rAttributes.shouldDisplayHidden() && isCellHidden(rCurrent))
530
0
                continue;
531
532
0
            double fCellValue = 0.0;
533
0
            SparklineValue::Action eAction = SparklineValue::Action::None;
534
0
            CellType eType = mrDocument.GetCellType(rCurrent);
535
536
0
            if (eType == CELLTYPE_NONE) // if cell is empty
537
0
            {
538
0
                auto eDisplayEmpty = rAttributes.getDisplayEmptyCellsAs();
539
0
                if (eDisplayEmpty == sc::DisplayEmptyCellsAs::Gap)
540
0
                    eAction = SparklineValue::Action::Skip;
541
0
                else if (eDisplayEmpty == sc::DisplayEmptyCellsAs::Span)
542
0
                    eAction = SparklineValue::Action::Interpolate;
543
0
            }
544
0
            else
545
0
            {
546
0
                fCellValue = mrDocument.GetValue(rCurrent);
547
0
            }
548
549
0
            aSparklineValues.add(fCellValue, eAction);
550
0
        }
551
552
0
        if (rAttributes.isRightToLeft())
553
0
            aSparklineValues.reverse();
554
555
0
        if (rAttributes.getType() == sc::SparklineType::Column)
556
0
        {
557
0
            drawColumn(rRenderContext, aOutputRectangle, aSparklineValues,
558
0
                       pSparklineGroup->getAttributes());
559
0
        }
560
0
        else if (rAttributes.getType() == sc::SparklineType::Stacked)
561
0
        {
562
0
            aSparklineValues.convertToStacked();
563
0
            drawColumn(rRenderContext, aOutputRectangle, aSparklineValues,
564
0
                       pSparklineGroup->getAttributes());
565
0
        }
566
0
        else if (rAttributes.getType() == sc::SparklineType::Line)
567
0
        {
568
0
            drawLine(rRenderContext, aOutputRectangle, aSparklineValues,
569
0
                     pSparklineGroup->getAttributes());
570
0
        }
571
0
    }
572
};
573
}
574
575
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */