LibreOffice Module sc (master) 1
SparklineRenderer.hxx
Go to the documentation of this file.
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
19
20#include <Sparkline.hxx>
21#include <SparklineGroup.hxx>
23
24namespace sc
25{
28{
31};
32
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;
45
46 SparklineValue(double aValue, Action eAction)
47 : maValue(aValue)
48 , meAction(eAction)
49 {
50 }
51};
52
61{
62private:
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
70public:
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>::min();
76
77 std::vector<SparklineValue> const& getValuesList() const { return maValueList; }
78
79 void add(double fValue, SparklineValue::Action eAction)
80 {
81 maValueList.emplace_back(fValue, eAction);
82 size_t nCurrentIndex = maValueList.size() - 1;
83
84 if (eAction == SparklineValue::Action::None)
85 {
86 mnLastIndex = nCurrentIndex;
87
90
91 if (fValue < mfMinimum)
92 mfMinimum = fValue;
93
94 if (fValue > mfMaximum)
95 mfMaximum = fValue;
96
97 interpolatePastValues(fValue, nCurrentIndex);
98
99 mnPreviousIndex = nCurrentIndex;
100 mfPreviousValue = fValue;
101 }
102 else if (eAction == SparklineValue::Action::Interpolate)
103 {
104 maToInterpolateIndex.push_back(nCurrentIndex);
106 }
107 }
108
109 static constexpr double interpolate(double x1, double y1, double x2, double y2, double x)
110 {
111 return (y1 * (x2 - x) + y2 * (x - x1)) / (x2 - x1);
112 }
113
114 void interpolatePastValues(double nCurrentValue, size_t nCurrentIndex)
115 {
116 if (maToInterpolateIndex.empty())
117 return;
118
119 if (mnPreviousIndex == std::numeric_limits<size_t>::max())
120 {
121 for (size_t nIndex : maToInterpolateIndex)
122 {
123 auto& rValue = maValueList[nIndex];
124 rValue.meAction = SparklineValue::Action::Skip;
125 }
126 }
127 else
128 {
129 for (size_t nIndex : maToInterpolateIndex)
130 {
131 double fInterpolated = interpolate(mnPreviousIndex, mfPreviousValue, nCurrentIndex,
132 nCurrentValue, nIndex);
133
134 auto& rValue = maValueList[nIndex];
135 rValue.maValue = fInterpolated;
136 rValue.meAction = SparklineValue::Action::None;
137 }
138 }
139 maToInterpolateIndex.clear();
140 }
141
143 {
144 // transform the data to 1, -1
145 for (auto& rValue : maValueList)
146 {
147 if (rValue.maValue != 0.0)
148 {
149 double fNewValue = rValue.maValue > 0.0 ? 1.0 : -1.0;
150
151 if (rValue.maValue == mfMinimum)
152 fNewValue -= 0.01;
153
154 if (rValue.maValue == mfMaximum)
155 fNewValue += 0.01;
156
157 rValue.maValue = fNewValue;
158 }
159 }
160 mfMinimum = -1.01;
161 mfMaximum = 1.01;
162 }
163
164 void reverse() { std::reverse(maValueList.begin(), maValueList.end()); }
165};
166
174{
175 enum class Direction
176 {
177 UNKNOWN,
178 ROW,
179 COLUMN
180 };
181
185
186public:
187 RangeTraverser(ScRange const& rRange)
188 : m_aCurrent(ScAddress::INITIALIZE_INVALID)
189 , m_aRange(rRange)
191
192 {
193 }
194
196 {
198
199 if (m_aRange.aStart.Row() == m_aRange.aEnd.Row())
200 {
203 }
204 else if (m_aRange.aStart.Col() == m_aRange.aEnd.Col())
205 {
208 }
209
210 return m_aCurrent;
211 }
212
213 bool hasNext()
214 {
216 return m_aCurrent.Col() <= m_aRange.aEnd.Col();
217 else if (m_eDirection == Direction::ROW)
218 return m_aCurrent.Row() <= m_aRange.aEnd.Row();
219 else
220 return false;
221 }
222
223 void next()
224 {
225 if (hasNext())
226 {
229 else if (m_eDirection == Direction::ROW)
231 }
232 }
233};
234
237{
238private:
242
243 double mfScaleX;
244 double mfScaleY;
245
246 void createMarker(std::vector<SparklineMarker>& rMarkers, double x, double y,
247 Color const& rColor)
248 {
249 auto& rMarker = rMarkers.emplace_back();
250 const double nHalfSizeX = double(mnOneX * 2 * mfScaleX);
251 const double nHalfSizeY = double(mnOneY * 2 * mfScaleY);
252 basegfx::B2DRectangle aRectangle(std::round(x - nHalfSizeX), std::round(y - nHalfSizeY),
253 std::round(x + nHalfSizeX), std::round(y + nHalfSizeY));
254 rMarker.maPolygon = basegfx::utils::createPolygonFromRect(aRectangle);
255 rMarker.maColor = rColor;
256 }
257
258 void drawLine(vcl::RenderContext& rRenderContext, tools::Rectangle const& rRectangle,
259 SparklineValues const& rSparklineValues,
260 sc::SparklineAttributes const& rAttributes)
261 {
262 double nMax = rSparklineValues.mfMaximum;
263 if (rAttributes.getMaxAxisType() == sc::AxisType::Custom && rAttributes.getManualMax())
264 nMax = *rAttributes.getManualMax();
265
266 double nMin = rSparklineValues.mfMinimum;
267 if (rAttributes.getMinAxisType() == sc::AxisType::Custom && rAttributes.getManualMin())
268 nMin = *rAttributes.getManualMin();
269
270 std::vector<SparklineValue> const& rValueList = rSparklineValues.getValuesList();
271 std::vector<basegfx::B2DPolygon> aPolygons;
272 aPolygons.emplace_back();
273 double numebrOfSteps = rValueList.size() - 1;
274 double xStep = 0;
275 double nDelta = nMax - nMin;
276
277 std::vector<SparklineMarker> aMarkers;
278 size_t nValueIndex = 0;
279
280 for (auto const& rSparklineValue : rValueList)
281 {
282 if (rSparklineValue.meAction == SparklineValue::Action::Skip)
283 {
284 aPolygons.emplace_back();
285 }
286 else
287 {
288 auto& aPolygon = aPolygons.back();
289 double nValue = rSparklineValue.maValue;
290
291 double nP = (nValue - nMin) / nDelta;
292 double x = rRectangle.GetWidth() * (xStep / numebrOfSteps);
293 double y = rRectangle.GetHeight() - rRectangle.GetHeight() * nP;
294
295 aPolygon.append({ x, y });
296
297 if (rAttributes.isFirst() && nValueIndex == rSparklineValues.mnFirstIndex)
298 {
299 createMarker(aMarkers, x, y, rAttributes.getColorFirst());
300 }
301 else if (rAttributes.isLast() && nValueIndex == rSparklineValues.mnLastIndex)
302 {
303 createMarker(aMarkers, x, y, rAttributes.getColorLast());
304 }
305 else if (rAttributes.isHigh() && nValue == rSparklineValues.mfMaximum)
306 {
307 createMarker(aMarkers, x, y, rAttributes.getColorHigh());
308 }
309 else if (rAttributes.isLow() && nValue == rSparklineValues.mfMinimum)
310 {
311 createMarker(aMarkers, x, y, rAttributes.getColorLow());
312 }
313 else if (rAttributes.isNegative() && nValue < 0.0)
314 {
315 createMarker(aMarkers, x, y, rAttributes.getColorNegative());
316 }
317 else if (rAttributes.isMarkers())
318 {
319 createMarker(aMarkers, x, y, rAttributes.getColorMarkers());
320 }
321 }
322
323 xStep++;
324 nValueIndex++;
325 }
326
327 basegfx::B2DHomMatrix aMatrix;
328 aMatrix.translate(rRectangle.Left(), rRectangle.Top());
329
330 if (rAttributes.shouldDisplayXAxis())
331 {
332 double nZero = 0 - nMin / nDelta;
333
334 if (nZero >= 0) // if nZero < 0, the axis is not visible
335 {
336 double x1 = 0.0;
337 double x2 = double(rRectangle.GetWidth());
338 double y = rRectangle.GetHeight() - rRectangle.GetHeight() * nZero;
339
340 basegfx::B2DPolygon aAxisPolygon;
341 aAxisPolygon.append({ x1, y });
342 aAxisPolygon.append({ x2, y });
343
344 rRenderContext.SetLineColor(rAttributes.getColorAxis());
345 rRenderContext.DrawPolyLineDirect(aMatrix, aAxisPolygon, 0.2 * mfScaleX);
346 }
347 }
348
349 rRenderContext.SetLineColor(rAttributes.getColorSeries());
350
351 for (auto& rPolygon : aPolygons)
352 {
353 rRenderContext.DrawPolyLineDirect(aMatrix, rPolygon,
354 rAttributes.getLineWeight() * mfScaleX, 0.0, nullptr,
356 }
357
358 for (auto& rMarker : aMarkers)
359 {
360 rRenderContext.SetLineColor(rMarker.maColor);
361 rRenderContext.SetFillColor(rMarker.maColor);
362 auto& rPolygon = rMarker.maPolygon;
363 rPolygon.transform(aMatrix);
364 rRenderContext.DrawPolygon(rPolygon);
365 }
366 }
367
368 static void setFillAndLineColor(vcl::RenderContext& rRenderContext,
369 sc::SparklineAttributes const& rAttributes, double nValue,
370 size_t nValueIndex, SparklineValues const& rSparklineValues)
371 {
372 if (rAttributes.isFirst() && nValueIndex == rSparklineValues.mnFirstIndex)
373 {
374 rRenderContext.SetLineColor(rAttributes.getColorFirst());
375 rRenderContext.SetFillColor(rAttributes.getColorFirst());
376 }
377 else if (rAttributes.isLast() && nValueIndex == rSparklineValues.mnLastIndex)
378 {
379 rRenderContext.SetLineColor(rAttributes.getColorLast());
380 rRenderContext.SetFillColor(rAttributes.getColorLast());
381 }
382 else if (rAttributes.isHigh() && nValue == rSparklineValues.mfMaximum)
383 {
384 rRenderContext.SetLineColor(rAttributes.getColorHigh());
385 rRenderContext.SetFillColor(rAttributes.getColorHigh());
386 }
387 else if (rAttributes.isLow() && nValue == rSparklineValues.mfMinimum)
388 {
389 rRenderContext.SetLineColor(rAttributes.getColorLow());
390 rRenderContext.SetFillColor(rAttributes.getColorLow());
391 }
392 else if (rAttributes.isNegative() && nValue < 0.0)
393 {
394 rRenderContext.SetLineColor(rAttributes.getColorNegative());
395 rRenderContext.SetFillColor(rAttributes.getColorNegative());
396 }
397 else
398 {
399 rRenderContext.SetLineColor(rAttributes.getColorSeries());
400 rRenderContext.SetFillColor(rAttributes.getColorSeries());
401 }
402 }
403
404 void drawColumn(vcl::RenderContext& rRenderContext, tools::Rectangle const& rRectangle,
405 SparklineValues const& rSparklineValues,
406 sc::SparklineAttributes const& rAttributes)
407 {
408 double nMax = rSparklineValues.mfMaximum;
409 if (rAttributes.getMaxAxisType() == sc::AxisType::Custom && rAttributes.getManualMax())
410 nMax = *rAttributes.getManualMax();
411
412 double nMin = rSparklineValues.mfMinimum;
413 if (rAttributes.getMinAxisType() == sc::AxisType::Custom && rAttributes.getManualMin())
414 nMin = *rAttributes.getManualMin();
415
416 std::vector<SparklineValue> const& rValueList = rSparklineValues.getValuesList();
417
418 basegfx::B2DPolygon aPolygon;
419 basegfx::B2DHomMatrix aMatrix;
420 aMatrix.translate(rRectangle.Left(), rRectangle.Top());
421
422 double xStep = 0;
423 double numberOfSteps = rValueList.size();
424 double nDelta = nMax - nMin;
425
426 double nColumnSize = rRectangle.GetWidth() / numberOfSteps;
427 nColumnSize = nColumnSize - (nColumnSize * 0.3);
428
429 double nZero = (0 - nMin) / nDelta;
430 double nZeroPosition = 0.0;
431 if (nZero >= 0)
432 {
433 nZeroPosition = rRectangle.GetHeight() - rRectangle.GetHeight() * nZero;
434
435 if (rAttributes.shouldDisplayXAxis())
436 {
437 double x1 = 0.0;
438 double x2 = double(rRectangle.GetWidth());
439
440 basegfx::B2DPolygon aAxisPolygon;
441 aAxisPolygon.append({ x1, nZeroPosition });
442 aAxisPolygon.append({ x2, nZeroPosition });
443
444 rRenderContext.SetLineColor(rAttributes.getColorAxis());
445 rRenderContext.DrawPolyLineDirect(aMatrix, aAxisPolygon, 0.2 * mfScaleX);
446 }
447 }
448 else
449 nZeroPosition = rRectangle.GetHeight();
450
451 size_t nValueIndex = 0;
452
453 for (auto const& rSparklineValue : rValueList)
454 {
455 double nValue = rSparklineValue.maValue;
456
457 if (nValue != 0.0)
458 {
459 setFillAndLineColor(rRenderContext, rAttributes, nValue, nValueIndex,
460 rSparklineValues);
461
462 double nP = (nValue - nMin) / nDelta;
463 double x = rRectangle.GetWidth() * (xStep / numberOfSteps);
464 double y = rRectangle.GetHeight() - rRectangle.GetHeight() * nP;
465
466 basegfx::B2DRectangle aRectangle(x, y, x + nColumnSize, nZeroPosition);
467 aPolygon = basegfx::utils::createPolygonFromRect(aRectangle);
468
469 aPolygon.transform(aMatrix);
470 rRenderContext.DrawPolygon(aPolygon);
471 }
472 xStep++;
473 nValueIndex++;
474 }
475 }
476
477 bool isCellHidden(ScAddress const& rAddress)
478 {
479 return mrDocument.RowHidden(rAddress.Row(), rAddress.Tab())
480 || mrDocument.ColHidden(rAddress.Col(), rAddress.Tab());
481 }
482
483public:
485 : mrDocument(rDocument)
486 , mnOneX(1)
487 , mnOneY(1)
488 , mfScaleX(1.0)
489 , mfScaleY(1.0)
490 {
491 }
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 {
497 rRenderContext.Push();
498 comphelper::ScopeGuard aPushPopGuard([&rRenderContext]() { rRenderContext.Pop(); });
499
500 rRenderContext.SetAntialiasing(AntialiasingFlags::Enable);
501 rRenderContext.SetClipRegion(vcl::Region(rRectangle));
502
503 tools::Rectangle aOutputRectangle(rRectangle);
504 aOutputRectangle.shrink(6); // provide border
505
506 mnOneX = nOneX;
507 mnOneY = nOneY;
508 mfScaleX = fScaleX;
509 mfScaleY = fScaleY;
510
511 auto const& rRangeList = pSparkline->getInputRange();
512
513 if (rRangeList.empty())
514 {
515 return;
516 }
517
518 auto pSparklineGroup = pSparkline->getSparklineGroup();
519 auto const& rAttributes = pSparklineGroup->getAttributes();
520
521 ScRange aRange = rRangeList[0];
522
523 SparklineValues aSparklineValues;
524
525 RangeTraverser aTraverser(aRange);
526 for (ScAddress const& rCurrent = aTraverser.first(); aTraverser.hasNext();
527 aTraverser.next())
528 {
529 // Skip if the cell is hidden and "displayHidden" attribute is not selected
530 if (!rAttributes.shouldDisplayHidden() && isCellHidden(rCurrent))
531 continue;
532
533 double fCellValue = 0.0;
536
537 if (eType == CELLTYPE_NONE) // if cell is empty
538 {
539 auto eDisplayEmpty = rAttributes.getDisplayEmptyCellsAs();
540 if (eDisplayEmpty == sc::DisplayEmptyCellsAs::Gap)
542 else if (eDisplayEmpty == sc::DisplayEmptyCellsAs::Span)
544 }
545 else
546 {
547 fCellValue = mrDocument.GetValue(rCurrent);
548 }
549
550 aSparklineValues.add(fCellValue, eAction);
551 }
552
553 if (rAttributes.isRightToLeft())
554 aSparklineValues.reverse();
555
556 if (rAttributes.getType() == sc::SparklineType::Column)
557 {
558 drawColumn(rRenderContext, aOutputRectangle, aSparklineValues,
559 pSparklineGroup->getAttributes());
560 }
561 else if (rAttributes.getType() == sc::SparklineType::Stacked)
562 {
563 aSparklineValues.convertToStacked();
564 drawColumn(rRenderContext, aOutputRectangle, aSparklineValues,
565 pSparklineGroup->getAttributes());
566 }
567 else if (rAttributes.getType() == sc::SparklineType::Line)
568 {
569 drawLine(rRenderContext, aOutputRectangle, aSparklineValues,
570 pSparklineGroup->getAttributes());
571 }
572 }
573};
574}
575
576/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
void SetAntialiasing(AntialiasingFlags nMode)
void SetLineColor()
void DrawPolygon(const tools::Polygon &rPoly)
void SetClipRegion()
bool DrawPolyLineDirect(const basegfx::B2DHomMatrix &rObjectTransform, const basegfx::B2DPolygon &rB2DPolygon, double fLineWidth=0.0, double fTransparency=0.0, const std::vector< double > *=nullptr, basegfx::B2DLineJoin eLineJoin=basegfx::B2DLineJoin::NONE, css::drawing::LineCap eLineCap=css::drawing::LineCap_BUTT, double fMiterMinimumAngle=basegfx::deg2rad(15.0))
void SetFillColor()
void Push(vcl::PushFlags nFlags=vcl::PushFlags::ALL)
SCTAB Tab() const
Definition: address.hxx:283
void IncCol(SCCOL nDelta=1)
Definition: address.hxx:316
SCROW Row() const
Definition: address.hxx:274
void SetInvalid()
Definition: address.hxx:299
void IncRow(SCROW nDelta=1)
Definition: address.hxx:312
SCCOL Col() const
Definition: address.hxx:279
SC_DLLPUBLIC double GetValue(const ScAddress &rPos) const
Definition: document.cxx:3671
SC_DLLPUBLIC bool RowHidden(SCROW nRow, SCTAB nTab, SCROW *pFirstRow=nullptr, SCROW *pLastRow=nullptr) const
Definition: document.cxx:4487
SC_DLLPUBLIC bool ColHidden(SCCOL nCol, SCTAB nTab, SCCOL *pFirstCol=nullptr, SCCOL *pLastCol=nullptr) const
Definition: document.cxx:4503
SC_DLLPUBLIC CellType GetCellType(SCCOL nCol, SCROW nRow, SCTAB nTab) const
Definition: document.cxx:3789
ScAddress aEnd
Definition: address.hxx:498
ScAddress aStart
Definition: address.hxx:497
void translate(double fX, double fY)
void transform(const basegfx::B2DHomMatrix &rMatrix)
void append(const basegfx::B2DPoint &rPoint, sal_uInt32 nCount)
Iterator to traverse the addresses in a range if the range is one dimensional.
ScAddress const & first()
RangeTraverser(ScRange const &rRange)
Common properties for a group of sparklines.
std::optional< double > getManualMax() const
std::optional< double > getManualMin() const
double getLineWeight() const
Line weight or width in points.
Render a provided sparkline into the input rectangle.
void drawLine(vcl::RenderContext &rRenderContext, tools::Rectangle const &rRectangle, SparklineValues const &rSparklineValues, sc::SparklineAttributes const &rAttributes)
void render(std::shared_ptr< sc::Sparkline > const &pSparkline, vcl::RenderContext &rRenderContext, tools::Rectangle const &rRectangle, tools::Long nOneX, tools::Long nOneY, double fScaleX, double fScaleY)
void drawColumn(vcl::RenderContext &rRenderContext, tools::Rectangle const &rRectangle, SparklineValues const &rSparklineValues, sc::SparklineAttributes const &rAttributes)
void createMarker(std::vector< SparklineMarker > &rMarkers, double x, double y, Color const &rColor)
SparklineRenderer(ScDocument &rDocument)
static void setFillAndLineColor(vcl::RenderContext &rRenderContext, sc::SparklineAttributes const &rAttributes, double nValue, size_t nValueIndex, SparklineValues const &rSparklineValues)
bool isCellHidden(ScAddress const &rAddress)
Contains and manages the values of the sparkline.
static constexpr double interpolate(double x1, double y1, double x2, double y2, double x)
void interpolatePastValues(double nCurrentValue, size_t nCurrentIndex)
void add(double fValue, SparklineValue::Action eAction)
std::vector< SparklineValue > const & getValuesList() const
std::vector< SparklineValue > maValueList
std::vector< size_t > maToInterpolateIndex
constexpr tools::Long GetWidth() const
constexpr tools::Long Top() const
void shrink(tools::Long nShrinkBy)
constexpr tools::Long GetHeight() const
constexpr tools::Long Left() const
float y
float x
UNKNOWN
DocumentType eType
sal_Int16 nValue
CellType
Definition: global.hxx:270
@ CELLTYPE_NONE
Definition: global.hxx:271
sal_Int32 nIndex
B2DPolygon createPolygonFromRect(const B2DRectangle &rRect, double fRadiusX, double fRadiusY)
None
CAUTION! The following defines must be in the same namespace as the respective type.
long Long
Contains the marker polygon and the color of a marker.
basegfx::B2DPolygon maPolygon
Sparkline value and action that needs to me performed on the value.
SparklineValue(double aValue, Action eAction)