1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
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/.
13 #include <document.hxx>
15 #include <basegfx/polygon/b2dpolygon.hxx>
16 #include <basegfx/polygon/b2dpolygontools.hxx>
17 #include <basegfx/matrix/b2dhommatrix.hxx>
18 #include <comphelper/scopeguard.hxx>
20 #include <Sparkline.hxx>
21 #include <SparklineGroup.hxx>
22 #include <SparklineAttributes.hxx>
26 /** Contains the marker polygon and the color of a marker */
27 struct SparklineMarker
29 basegfx::B2DPolygon maPolygon
;
33 /** Sparkline value and action that needs to me performed on the value */
38 None
, // No action on the value
39 Skip
, // Skip the value
40 Interpolate
// Interpolate the value
46 SparklineValue(double aValue
, Action eAction
)
53 /** Contains and manages the values of the sparkline.
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
63 double mfPreviousValue
= 0.0;
64 size_t mnPreviousIndex
= std::numeric_limits
<size_t>::max();
66 std::vector
<size_t> maToInterpolateIndex
;
68 std::vector
<SparklineValue
> maValueList
;
71 size_t mnFirstIndex
= std::numeric_limits
<size_t>::max();
72 size_t mnLastIndex
= 0;
74 double mfMinimum
= std::numeric_limits
<double>::max();
75 double mfMaximum
= std::numeric_limits
<double>::lowest();
77 std::vector
<SparklineValue
> const& getValuesList() const { return maValueList
; }
79 void add(double fValue
, SparklineValue::Action eAction
)
81 maValueList
.emplace_back(fValue
, eAction
);
82 size_t nCurrentIndex
= maValueList
.size() - 1;
84 if (eAction
== SparklineValue::Action::None
)
86 mnLastIndex
= nCurrentIndex
;
88 if (mnLastIndex
< mnFirstIndex
)
89 mnFirstIndex
= mnLastIndex
;
91 if (fValue
< mfMinimum
)
94 if (fValue
> mfMaximum
)
97 interpolatePastValues(fValue
, nCurrentIndex
);
99 mnPreviousIndex
= nCurrentIndex
;
100 mfPreviousValue
= fValue
;
102 else if (eAction
== SparklineValue::Action::Interpolate
)
104 maToInterpolateIndex
.push_back(nCurrentIndex
);
105 maValueList
.back().meAction
= SparklineValue::Action::Skip
;
109 static constexpr double interpolate(double x1
, double y1
, double x2
, double y2
, double x
)
111 return (y1
* (x2
- x
) + y2
* (x
- x1
)) / (x2
- x1
);
114 void interpolatePastValues(double nCurrentValue
, size_t nCurrentIndex
)
116 if (maToInterpolateIndex
.empty())
119 if (mnPreviousIndex
== std::numeric_limits
<size_t>::max())
121 for (size_t nIndex
: maToInterpolateIndex
)
123 auto& rValue
= maValueList
[nIndex
];
124 rValue
.meAction
= SparklineValue::Action::Skip
;
129 for (size_t nIndex
: maToInterpolateIndex
)
131 double fInterpolated
= interpolate(mnPreviousIndex
, mfPreviousValue
, nCurrentIndex
,
132 nCurrentValue
, nIndex
);
134 auto& rValue
= maValueList
[nIndex
];
135 rValue
.maValue
= fInterpolated
;
136 rValue
.meAction
= SparklineValue::Action::None
;
139 maToInterpolateIndex
.clear();
142 void convertToStacked()
144 // transform the data to 1, -1
145 for (auto& rValue
: maValueList
)
147 if (rValue
.maValue
!= 0.0)
149 double fNewValue
= rValue
.maValue
> 0.0 ? 1.0 : -1.0;
151 if (rValue
.maValue
== mfMinimum
)
154 if (rValue
.maValue
== mfMaximum
)
157 rValue
.maValue
= fNewValue
;
164 void reverse() { std::reverse(maValueList
.begin(), maValueList
.end()); }
167 /** Iterator to traverse the addresses in a range if the range is one dimensional.
169 * The direction to traverse is detected automatically or hasNext returns
170 * false if it is not possible to detect.
182 ScAddress m_aCurrent
;
184 Direction m_eDirection
;
187 RangeTraverser(ScRange
const& rRange
)
188 : m_aCurrent(ScAddress::INITIALIZE_INVALID
)
190 , m_eDirection(Direction::UNKNOWN
)
195 ScAddress
const& first()
197 m_aCurrent
.SetInvalid();
199 if (m_aRange
.aStart
.Row() == m_aRange
.aEnd
.Row())
201 m_eDirection
= Direction::COLUMN
;
202 m_aCurrent
= m_aRange
.aStart
;
204 else if (m_aRange
.aStart
.Col() == m_aRange
.aEnd
.Col())
206 m_eDirection
= Direction::ROW
;
207 m_aCurrent
= m_aRange
.aStart
;
215 if (m_eDirection
== Direction::COLUMN
)
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();
227 if (m_eDirection
== Direction::COLUMN
)
229 else if (m_eDirection
== Direction::ROW
)
235 /** Render a provided sparkline into the input rectangle */
236 class SparklineRenderer
239 ScDocument
& mrDocument
;
246 void createMarker(std::vector
<SparklineMarker
>& rMarkers
, double x
, double y
,
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
;
258 void drawLine(vcl::RenderContext
& rRenderContext
, tools::Rectangle
const& rRectangle
,
259 SparklineValues
const& rSparklineValues
,
260 sc::SparklineAttributes
const& rAttributes
)
262 double nMax
= rSparklineValues
.mfMaximum
;
263 if (rAttributes
.getMaxAxisType() == sc::AxisType::Custom
&& rAttributes
.getManualMax())
264 nMax
= *rAttributes
.getManualMax();
266 double nMin
= rSparklineValues
.mfMinimum
;
267 if (rAttributes
.getMinAxisType() == sc::AxisType::Custom
&& rAttributes
.getManualMin())
268 nMin
= *rAttributes
.getManualMin();
270 std::vector
<SparklineValue
> const& rValueList
= rSparklineValues
.getValuesList();
271 std::vector
<basegfx::B2DPolygon
> aPolygons
;
272 aPolygons
.emplace_back();
273 double numebrOfSteps
= rValueList
.size() - 1;
275 double nDelta
= nMax
- nMin
;
277 std::vector
<SparklineMarker
> aMarkers
;
278 size_t nValueIndex
= 0;
280 for (auto const& rSparklineValue
: rValueList
)
282 if (rSparklineValue
.meAction
== SparklineValue::Action::Skip
)
284 aPolygons
.emplace_back();
288 auto& aPolygon
= aPolygons
.back();
289 double nValue
= rSparklineValue
.maValue
;
291 double nP
= (nValue
- nMin
) / nDelta
;
292 double x
= rRectangle
.GetWidth() * (xStep
/ numebrOfSteps
);
293 double y
= rRectangle
.GetHeight() - rRectangle
.GetHeight() * nP
;
295 aPolygon
.append({ x
, y
});
297 if (rAttributes
.isFirst() && nValueIndex
== rSparklineValues
.mnFirstIndex
)
299 createMarker(aMarkers
, x
, y
, rAttributes
.getColorFirst().getFinalColor());
301 else if (rAttributes
.isLast() && nValueIndex
== rSparklineValues
.mnLastIndex
)
303 createMarker(aMarkers
, x
, y
, rAttributes
.getColorLast().getFinalColor());
305 else if (rAttributes
.isHigh() && nValue
== rSparklineValues
.mfMaximum
)
307 createMarker(aMarkers
, x
, y
, rAttributes
.getColorHigh().getFinalColor());
309 else if (rAttributes
.isLow() && nValue
== rSparklineValues
.mfMinimum
)
311 createMarker(aMarkers
, x
, y
, rAttributes
.getColorLow().getFinalColor());
313 else if (rAttributes
.isNegative() && nValue
< 0.0)
315 createMarker(aMarkers
, x
, y
, rAttributes
.getColorNegative().getFinalColor());
317 else if (rAttributes
.isMarkers())
319 createMarker(aMarkers
, x
, y
, rAttributes
.getColorMarkers().getFinalColor());
327 basegfx::B2DHomMatrix aMatrix
;
328 aMatrix
.translate(rRectangle
.Left(), rRectangle
.Top());
330 if (rAttributes
.shouldDisplayXAxis())
332 double nZero
= 0 - nMin
/ nDelta
;
334 if (nZero
>= 0) // if nZero < 0, the axis is not visible
337 double x2
= double(rRectangle
.GetWidth());
338 double y
= rRectangle
.GetHeight() - rRectangle
.GetHeight() * nZero
;
340 basegfx::B2DPolygon aAxisPolygon
;
341 aAxisPolygon
.append({ x1
, y
});
342 aAxisPolygon
.append({ x2
, y
});
344 rRenderContext
.SetLineColor(rAttributes
.getColorAxis().getFinalColor());
345 rRenderContext
.DrawPolyLineDirect(aMatrix
, aAxisPolygon
, 0.2 * mfScaleX
);
349 rRenderContext
.SetLineColor(rAttributes
.getColorSeries().getFinalColor());
351 for (auto& rPolygon
: aPolygons
)
353 rRenderContext
.DrawPolyLineDirect(aMatrix
, rPolygon
,
354 rAttributes
.getLineWeight() * mfScaleX
, 0.0, nullptr,
355 basegfx::B2DLineJoin::Round
);
358 for (auto& rMarker
: aMarkers
)
360 rRenderContext
.SetLineColor(rMarker
.maColor
);
361 rRenderContext
.SetFillColor(rMarker
.maColor
);
362 auto& rPolygon
= rMarker
.maPolygon
;
363 rPolygon
.transform(aMatrix
);
364 rRenderContext
.DrawPolygon(rPolygon
);
368 static void setFillAndLineColor(vcl::RenderContext
& rRenderContext
,
369 sc::SparklineAttributes
const& rAttributes
, double nValue
,
370 size_t nValueIndex
, SparklineValues
const& rSparklineValues
)
372 if (rAttributes
.isFirst() && nValueIndex
== rSparklineValues
.mnFirstIndex
)
374 rRenderContext
.SetLineColor(rAttributes
.getColorFirst().getFinalColor());
375 rRenderContext
.SetFillColor(rAttributes
.getColorFirst().getFinalColor());
377 else if (rAttributes
.isLast() && nValueIndex
== rSparklineValues
.mnLastIndex
)
379 rRenderContext
.SetLineColor(rAttributes
.getColorLast().getFinalColor());
380 rRenderContext
.SetFillColor(rAttributes
.getColorLast().getFinalColor());
382 else if (rAttributes
.isHigh() && nValue
== rSparklineValues
.mfMaximum
)
384 rRenderContext
.SetLineColor(rAttributes
.getColorHigh().getFinalColor());
385 rRenderContext
.SetFillColor(rAttributes
.getColorHigh().getFinalColor());
387 else if (rAttributes
.isLow() && nValue
== rSparklineValues
.mfMinimum
)
389 rRenderContext
.SetLineColor(rAttributes
.getColorLow().getFinalColor());
390 rRenderContext
.SetFillColor(rAttributes
.getColorLow().getFinalColor());
392 else if (rAttributes
.isNegative() && nValue
< 0.0)
394 rRenderContext
.SetLineColor(rAttributes
.getColorNegative().getFinalColor());
395 rRenderContext
.SetFillColor(rAttributes
.getColorNegative().getFinalColor());
399 rRenderContext
.SetLineColor(rAttributes
.getColorSeries().getFinalColor());
400 rRenderContext
.SetFillColor(rAttributes
.getColorSeries().getFinalColor());
404 void drawColumn(vcl::RenderContext
& rRenderContext
, tools::Rectangle
const& rRectangle
,
405 SparklineValues
const& rSparklineValues
,
406 sc::SparklineAttributes
const& rAttributes
)
408 double nMax
= rSparklineValues
.mfMaximum
;
409 if (rAttributes
.getMaxAxisType() == sc::AxisType::Custom
&& rAttributes
.getManualMax())
410 nMax
= *rAttributes
.getManualMax();
412 double nMin
= rSparklineValues
.mfMinimum
;
413 if (rAttributes
.getMinAxisType() == sc::AxisType::Custom
&& rAttributes
.getManualMin())
414 nMin
= *rAttributes
.getManualMin();
416 std::vector
<SparklineValue
> const& rValueList
= rSparklineValues
.getValuesList();
418 basegfx::B2DPolygon aPolygon
;
419 basegfx::B2DHomMatrix aMatrix
;
420 aMatrix
.translate(rRectangle
.Left(), rRectangle
.Top());
423 double numberOfSteps
= rValueList
.size();
424 double nDelta
= nMax
- nMin
;
426 double nColumnSize
= rRectangle
.GetWidth() / numberOfSteps
;
427 nColumnSize
= nColumnSize
- (nColumnSize
* 0.3);
429 double nZero
= (0 - nMin
) / nDelta
;
430 double nZeroPosition
= 0.0;
433 nZeroPosition
= rRectangle
.GetHeight() - rRectangle
.GetHeight() * nZero
;
435 if (rAttributes
.shouldDisplayXAxis())
438 double x2
= double(rRectangle
.GetWidth());
440 basegfx::B2DPolygon aAxisPolygon
;
441 aAxisPolygon
.append({ x1
, nZeroPosition
});
442 aAxisPolygon
.append({ x2
, nZeroPosition
});
444 rRenderContext
.SetLineColor(rAttributes
.getColorAxis().getFinalColor());
445 rRenderContext
.DrawPolyLineDirect(aMatrix
, aAxisPolygon
, 0.2 * mfScaleX
);
449 nZeroPosition
= rRectangle
.GetHeight();
451 size_t nValueIndex
= 0;
453 for (auto const& rSparklineValue
: rValueList
)
455 double nValue
= rSparklineValue
.maValue
;
459 setFillAndLineColor(rRenderContext
, rAttributes
, nValue
, nValueIndex
,
462 double nP
= (nValue
- nMin
) / nDelta
;
463 double x
= rRectangle
.GetWidth() * (xStep
/ numberOfSteps
);
464 double y
= rRectangle
.GetHeight() - rRectangle
.GetHeight() * nP
;
466 basegfx::B2DRectangle
aRectangle(x
, y
, x
+ nColumnSize
, nZeroPosition
);
467 aPolygon
= basegfx::utils::createPolygonFromRect(aRectangle
);
469 aPolygon
.transform(aMatrix
);
470 rRenderContext
.DrawPolygon(aPolygon
);
477 bool isCellHidden(ScAddress
const& rAddress
)
479 return mrDocument
.RowHidden(rAddress
.Row(), rAddress
.Tab())
480 || mrDocument
.ColHidden(rAddress
.Col(), rAddress
.Tab());
484 SparklineRenderer(ScDocument
& rDocument
)
485 : mrDocument(rDocument
)
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
)
497 rRenderContext
.Push();
498 comphelper::ScopeGuard
aPushPopGuard([&rRenderContext
]() { rRenderContext
.Pop(); });
500 rRenderContext
.SetAntialiasing(AntialiasingFlags::Enable
);
501 rRenderContext
.SetClipRegion(vcl::Region(rRectangle
));
503 tools::Rectangle
aOutputRectangle(rRectangle
);
504 aOutputRectangle
.shrink(6); // provide border
511 auto const& rRangeList
= pSparkline
->getInputRange();
513 if (rRangeList
.empty())
518 auto pSparklineGroup
= pSparkline
->getSparklineGroup();
519 auto const& rAttributes
= pSparklineGroup
->getAttributes();
521 ScRange aRange
= rRangeList
[0];
523 SparklineValues aSparklineValues
;
525 RangeTraverser
aTraverser(aRange
);
526 for (ScAddress
const& rCurrent
= aTraverser
.first(); aTraverser
.hasNext();
529 // Skip if the cell is hidden and "displayHidden" attribute is not selected
530 if (!rAttributes
.shouldDisplayHidden() && isCellHidden(rCurrent
))
533 double fCellValue
= 0.0;
534 SparklineValue::Action eAction
= SparklineValue::Action::None
;
535 CellType eType
= mrDocument
.GetCellType(rCurrent
);
537 if (eType
== CELLTYPE_NONE
) // if cell is empty
539 auto eDisplayEmpty
= rAttributes
.getDisplayEmptyCellsAs();
540 if (eDisplayEmpty
== sc::DisplayEmptyCellsAs::Gap
)
541 eAction
= SparklineValue::Action::Skip
;
542 else if (eDisplayEmpty
== sc::DisplayEmptyCellsAs::Span
)
543 eAction
= SparklineValue::Action::Interpolate
;
547 fCellValue
= mrDocument
.GetValue(rCurrent
);
550 aSparklineValues
.add(fCellValue
, eAction
);
553 if (rAttributes
.isRightToLeft())
554 aSparklineValues
.reverse();
556 if (rAttributes
.getType() == sc::SparklineType::Column
)
558 drawColumn(rRenderContext
, aOutputRectangle
, aSparklineValues
,
559 pSparklineGroup
->getAttributes());
561 else if (rAttributes
.getType() == sc::SparklineType::Stacked
)
563 aSparklineValues
.convertToStacked();
564 drawColumn(rRenderContext
, aOutputRectangle
, aSparklineValues
,
565 pSparklineGroup
->getAttributes());
567 else if (rAttributes
.getType() == sc::SparklineType::Line
)
569 drawLine(rRenderContext
, aOutputRectangle
, aSparklineValues
,
570 pSparklineGroup
->getAttributes());
576 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */