diff options
Diffstat (limited to 'sc/source/ui/inc/SparklineRenderer.hxx')
-rw-r--r-- | sc/source/ui/inc/SparklineRenderer.hxx | 576 |
1 files changed, 576 insertions, 0 deletions
diff --git a/sc/source/ui/inc/SparklineRenderer.hxx b/sc/source/ui/inc/SparklineRenderer.hxx new file mode 100644 index 000000000..616d667ec --- /dev/null +++ b/sc/source/ui/inc/SparklineRenderer.hxx @@ -0,0 +1,576 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + +#pragma once + +#include <document.hxx> + +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <comphelper/scopeguard.hxx> + +#include <Sparkline.hxx> +#include <SparklineGroup.hxx> +#include <SparklineAttributes.hxx> + +namespace sc +{ +/** Contains the marker polygon and the color of a marker */ +struct SparklineMarker +{ + basegfx::B2DPolygon maPolygon; + Color maColor; +}; + +/** Sparkline value and action that needs to me performed on the value */ +struct SparklineValue +{ + enum class Action + { + None, // No action on the value + Skip, // Skip the value + Interpolate // Interpolate the value + }; + + double maValue; + Action meAction; + + SparklineValue(double aValue, Action eAction) + : maValue(aValue) + , meAction(eAction) + { + } +}; + +/** Contains and manages the values of the sparkline. + * + * It automatically keeps track of the minimums and maximums, and + * skips or interpolates the sparkline values if needed, depending on + * the input. This is done so it is easier to handle the sparkline + * values later on. + */ +class SparklineValues +{ +private: + double mfPreviousValue = 0.0; + size_t mnPreviousIndex = std::numeric_limits<size_t>::max(); + + std::vector<size_t> maToInterpolateIndex; + + std::vector<SparklineValue> maValueList; + +public: + size_t mnFirstIndex = std::numeric_limits<size_t>::max(); + size_t mnLastIndex = 0; + + double mfMinimum = std::numeric_limits<double>::max(); + double mfMaximum = std::numeric_limits<double>::min(); + + std::vector<SparklineValue> const& getValuesList() const { return maValueList; } + + void add(double fValue, SparklineValue::Action eAction) + { + maValueList.emplace_back(fValue, eAction); + size_t nCurrentIndex = maValueList.size() - 1; + + if (eAction == SparklineValue::Action::None) + { + mnLastIndex = nCurrentIndex; + + if (mnLastIndex < mnFirstIndex) + mnFirstIndex = mnLastIndex; + + if (fValue < mfMinimum) + mfMinimum = fValue; + + if (fValue > mfMaximum) + mfMaximum = fValue; + + interpolatePastValues(fValue, nCurrentIndex); + + mnPreviousIndex = nCurrentIndex; + mfPreviousValue = fValue; + } + else if (eAction == SparklineValue::Action::Interpolate) + { + maToInterpolateIndex.push_back(nCurrentIndex); + maValueList.back().meAction = SparklineValue::Action::Skip; + } + } + + static constexpr double interpolate(double x1, double y1, double x2, double y2, double x) + { + return (y1 * (x2 - x) + y2 * (x - x1)) / (x2 - x1); + } + + void interpolatePastValues(double nCurrentValue, size_t nCurrentIndex) + { + if (maToInterpolateIndex.empty()) + return; + + if (mnPreviousIndex == std::numeric_limits<size_t>::max()) + { + for (size_t nIndex : maToInterpolateIndex) + { + auto& rValue = maValueList[nIndex]; + rValue.meAction = SparklineValue::Action::Skip; + } + } + else + { + for (size_t nIndex : maToInterpolateIndex) + { + double fInterpolated = interpolate(mnPreviousIndex, mfPreviousValue, nCurrentIndex, + nCurrentValue, nIndex); + + auto& rValue = maValueList[nIndex]; + rValue.maValue = fInterpolated; + rValue.meAction = SparklineValue::Action::None; + } + } + maToInterpolateIndex.clear(); + } + + void convertToStacked() + { + // transform the data to 1, -1 + for (auto& rValue : maValueList) + { + if (rValue.maValue != 0.0) + { + double fNewValue = rValue.maValue > 0.0 ? 1.0 : -1.0; + + if (rValue.maValue == mfMinimum) + fNewValue -= 0.01; + + if (rValue.maValue == mfMaximum) + fNewValue += 0.01; + + rValue.maValue = fNewValue; + } + } + mfMinimum = -1.01; + mfMaximum = 1.01; + } + + void reverse() { std::reverse(maValueList.begin(), maValueList.end()); } +}; + +/** Iterator to traverse the addresses in a range if the range is one dimensional. + * + * The direction to traverse is detected automatically or hasNext returns + * false if it is not possible to detect. + * + */ +class RangeTraverser +{ + enum class Direction + { + UNKNOWN, + ROW, + COLUMN + }; + + ScAddress m_aCurrent; + ScRange m_aRange; + Direction m_eDirection; + +public: + RangeTraverser(ScRange const& rRange) + : m_aCurrent(ScAddress::INITIALIZE_INVALID) + , m_aRange(rRange) + , m_eDirection(Direction::UNKNOWN) + + { + } + + ScAddress const& first() + { + m_aCurrent.SetInvalid(); + + if (m_aRange.aStart.Row() == m_aRange.aEnd.Row()) + { + m_eDirection = Direction::COLUMN; + m_aCurrent = m_aRange.aStart; + } + else if (m_aRange.aStart.Col() == m_aRange.aEnd.Col()) + { + m_eDirection = Direction::ROW; + m_aCurrent = m_aRange.aStart; + } + + return m_aCurrent; + } + + bool hasNext() + { + if (m_eDirection == Direction::COLUMN) + return m_aCurrent.Col() <= m_aRange.aEnd.Col(); + else if (m_eDirection == Direction::ROW) + return m_aCurrent.Row() <= m_aRange.aEnd.Row(); + else + return false; + } + + void next() + { + if (hasNext()) + { + if (m_eDirection == Direction::COLUMN) + m_aCurrent.IncCol(); + else if (m_eDirection == Direction::ROW) + m_aCurrent.IncRow(); + } + } +}; + +/** Render a provided sparkline into the input rectangle */ +class SparklineRenderer +{ +private: + ScDocument& mrDocument; + tools::Long mnOneX; + tools::Long mnOneY; + + double mfScaleX; + double mfScaleY; + + void createMarker(std::vector<SparklineMarker>& rMarkers, double x, double y, + Color const& rColor) + { + auto& rMarker = rMarkers.emplace_back(); + const double nHalfSizeX = double(mnOneX * 2 * mfScaleX); + const double nHalfSizeY = double(mnOneY * 2 * mfScaleY); + basegfx::B2DRectangle aRectangle(std::round(x - nHalfSizeX), std::round(y - nHalfSizeY), + std::round(x + nHalfSizeX), std::round(y + nHalfSizeY)); + rMarker.maPolygon = basegfx::utils::createPolygonFromRect(aRectangle); + rMarker.maColor = rColor; + } + + void drawLine(vcl::RenderContext& rRenderContext, tools::Rectangle const& rRectangle, + SparklineValues const& rSparklineValues, + sc::SparklineAttributes const& rAttributes) + { + double nMax = rSparklineValues.mfMaximum; + if (rAttributes.getMaxAxisType() == sc::AxisType::Custom && rAttributes.getManualMax()) + nMax = *rAttributes.getManualMax(); + + double nMin = rSparklineValues.mfMinimum; + if (rAttributes.getMinAxisType() == sc::AxisType::Custom && rAttributes.getManualMin()) + nMin = *rAttributes.getManualMin(); + + std::vector<SparklineValue> const& rValueList = rSparklineValues.getValuesList(); + std::vector<basegfx::B2DPolygon> aPolygons; + aPolygons.emplace_back(); + double numebrOfSteps = rValueList.size() - 1; + double xStep = 0; + double nDelta = nMax - nMin; + + std::vector<SparklineMarker> aMarkers; + size_t nValueIndex = 0; + + for (auto const& rSparklineValue : rValueList) + { + if (rSparklineValue.meAction == SparklineValue::Action::Skip) + { + aPolygons.emplace_back(); + } + else + { + auto& aPolygon = aPolygons.back(); + double nValue = rSparklineValue.maValue; + + double nP = (nValue - nMin) / nDelta; + double x = rRectangle.GetWidth() * (xStep / numebrOfSteps); + double y = rRectangle.GetHeight() - rRectangle.GetHeight() * nP; + + aPolygon.append({ x, y }); + + if (rAttributes.isFirst() && nValueIndex == rSparklineValues.mnFirstIndex) + { + createMarker(aMarkers, x, y, rAttributes.getColorFirst()); + } + else if (rAttributes.isLast() && nValueIndex == rSparklineValues.mnLastIndex) + { + createMarker(aMarkers, x, y, rAttributes.getColorLast()); + } + else if (rAttributes.isHigh() && nValue == rSparklineValues.mfMaximum) + { + createMarker(aMarkers, x, y, rAttributes.getColorHigh()); + } + else if (rAttributes.isLow() && nValue == rSparklineValues.mfMinimum) + { + createMarker(aMarkers, x, y, rAttributes.getColorLow()); + } + else if (rAttributes.isNegative() && nValue < 0.0) + { + createMarker(aMarkers, x, y, rAttributes.getColorNegative()); + } + else if (rAttributes.isMarkers()) + { + createMarker(aMarkers, x, y, rAttributes.getColorMarkers()); + } + } + + xStep++; + nValueIndex++; + } + + basegfx::B2DHomMatrix aMatrix; + aMatrix.translate(rRectangle.Left(), rRectangle.Top()); + + if (rAttributes.shouldDisplayXAxis()) + { + double nZero = 0 - nMin / nDelta; + + if (nZero >= 0) // if nZero < 0, the axis is not visible + { + double x1 = 0.0; + double x2 = double(rRectangle.GetWidth()); + double y = rRectangle.GetHeight() - rRectangle.GetHeight() * nZero; + + basegfx::B2DPolygon aAxisPolygon; + aAxisPolygon.append({ x1, y }); + aAxisPolygon.append({ x2, y }); + + rRenderContext.SetLineColor(rAttributes.getColorAxis()); + rRenderContext.DrawPolyLineDirect(aMatrix, aAxisPolygon, 0.2 * mfScaleX); + } + } + + rRenderContext.SetLineColor(rAttributes.getColorSeries()); + + for (auto& rPolygon : aPolygons) + { + rRenderContext.DrawPolyLineDirect(aMatrix, rPolygon, + rAttributes.getLineWeight() * mfScaleX, 0.0, nullptr, + basegfx::B2DLineJoin::Round); + } + + for (auto& rMarker : aMarkers) + { + rRenderContext.SetLineColor(rMarker.maColor); + rRenderContext.SetFillColor(rMarker.maColor); + auto& rPolygon = rMarker.maPolygon; + rPolygon.transform(aMatrix); + rRenderContext.DrawPolygon(rPolygon); + } + } + + static void setFillAndLineColor(vcl::RenderContext& rRenderContext, + sc::SparklineAttributes const& rAttributes, double nValue, + size_t nValueIndex, SparklineValues const& rSparklineValues) + { + if (rAttributes.isFirst() && nValueIndex == rSparklineValues.mnFirstIndex) + { + rRenderContext.SetLineColor(rAttributes.getColorFirst()); + rRenderContext.SetFillColor(rAttributes.getColorFirst()); + } + else if (rAttributes.isLast() && nValueIndex == rSparklineValues.mnLastIndex) + { + rRenderContext.SetLineColor(rAttributes.getColorLast()); + rRenderContext.SetFillColor(rAttributes.getColorLast()); + } + else if (rAttributes.isHigh() && nValue == rSparklineValues.mfMaximum) + { + rRenderContext.SetLineColor(rAttributes.getColorHigh()); + rRenderContext.SetFillColor(rAttributes.getColorHigh()); + } + else if (rAttributes.isLow() && nValue == rSparklineValues.mfMinimum) + { + rRenderContext.SetLineColor(rAttributes.getColorLow()); + rRenderContext.SetFillColor(rAttributes.getColorLow()); + } + else if (rAttributes.isNegative() && nValue < 0.0) + { + rRenderContext.SetLineColor(rAttributes.getColorNegative()); + rRenderContext.SetFillColor(rAttributes.getColorNegative()); + } + else + { + rRenderContext.SetLineColor(rAttributes.getColorSeries()); + rRenderContext.SetFillColor(rAttributes.getColorSeries()); + } + } + + void drawColumn(vcl::RenderContext& rRenderContext, tools::Rectangle const& rRectangle, + SparklineValues const& rSparklineValues, + sc::SparklineAttributes const& rAttributes) + { + double nMax = rSparklineValues.mfMaximum; + if (rAttributes.getMaxAxisType() == sc::AxisType::Custom && rAttributes.getManualMax()) + nMax = *rAttributes.getManualMax(); + + double nMin = rSparklineValues.mfMinimum; + if (rAttributes.getMinAxisType() == sc::AxisType::Custom && rAttributes.getManualMin()) + nMin = *rAttributes.getManualMin(); + + std::vector<SparklineValue> const& rValueList = rSparklineValues.getValuesList(); + + basegfx::B2DPolygon aPolygon; + basegfx::B2DHomMatrix aMatrix; + aMatrix.translate(rRectangle.Left(), rRectangle.Top()); + + double xStep = 0; + double numberOfSteps = rValueList.size(); + double nDelta = nMax - nMin; + + double nColumnSize = rRectangle.GetWidth() / numberOfSteps; + nColumnSize = nColumnSize - (nColumnSize * 0.3); + + double nZero = (0 - nMin) / nDelta; + double nZeroPosition = 0.0; + if (nZero >= 0) + { + nZeroPosition = rRectangle.GetHeight() - rRectangle.GetHeight() * nZero; + + if (rAttributes.shouldDisplayXAxis()) + { + double x1 = 0.0; + double x2 = double(rRectangle.GetWidth()); + + basegfx::B2DPolygon aAxisPolygon; + aAxisPolygon.append({ x1, nZeroPosition }); + aAxisPolygon.append({ x2, nZeroPosition }); + + rRenderContext.SetLineColor(rAttributes.getColorAxis()); + rRenderContext.DrawPolyLineDirect(aMatrix, aAxisPolygon, 0.2 * mfScaleX); + } + } + else + nZeroPosition = rRectangle.GetHeight(); + + size_t nValueIndex = 0; + + for (auto const& rSparklineValue : rValueList) + { + double nValue = rSparklineValue.maValue; + + if (nValue != 0.0) + { + setFillAndLineColor(rRenderContext, rAttributes, nValue, nValueIndex, + rSparklineValues); + + double nP = (nValue - nMin) / nDelta; + double x = rRectangle.GetWidth() * (xStep / numberOfSteps); + double y = rRectangle.GetHeight() - rRectangle.GetHeight() * nP; + + basegfx::B2DRectangle aRectangle(x, y, x + nColumnSize, nZeroPosition); + aPolygon = basegfx::utils::createPolygonFromRect(aRectangle); + + aPolygon.transform(aMatrix); + rRenderContext.DrawPolygon(aPolygon); + } + xStep++; + nValueIndex++; + } + } + + bool isCellHidden(ScAddress const& rAddress) + { + return mrDocument.RowHidden(rAddress.Row(), rAddress.Tab()) + || mrDocument.ColHidden(rAddress.Col(), rAddress.Tab()); + } + +public: + SparklineRenderer(ScDocument& rDocument) + : mrDocument(rDocument) + , mnOneX(1) + , mnOneY(1) + , mfScaleX(1.0) + , mfScaleY(1.0) + { + } + + 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) + { + rRenderContext.Push(); + comphelper::ScopeGuard aPushPopGuard([&rRenderContext]() { rRenderContext.Pop(); }); + + rRenderContext.SetAntialiasing(AntialiasingFlags::Enable); + rRenderContext.SetClipRegion(vcl::Region(rRectangle)); + + tools::Rectangle aOutputRectangle(rRectangle); + aOutputRectangle.shrink(6); // provide border + + mnOneX = nOneX; + mnOneY = nOneY; + mfScaleX = fScaleX; + mfScaleY = fScaleY; + + auto const& rRangeList = pSparkline->getInputRange(); + + if (rRangeList.empty()) + { + return; + } + + auto pSparklineGroup = pSparkline->getSparklineGroup(); + auto const& rAttributes = pSparklineGroup->getAttributes(); + + ScRange aRange = rRangeList[0]; + + SparklineValues aSparklineValues; + + RangeTraverser aTraverser(aRange); + for (ScAddress const& rCurrent = aTraverser.first(); aTraverser.hasNext(); + aTraverser.next()) + { + // Skip if the cell is hidden and "displayHidden" attribute is not selected + if (!rAttributes.shouldDisplayHidden() && isCellHidden(rCurrent)) + continue; + + double fCellValue = 0.0; + SparklineValue::Action eAction = SparklineValue::Action::None; + CellType eType = mrDocument.GetCellType(rCurrent); + + if (eType == CELLTYPE_NONE) // if cell is empty + { + auto eDisplayEmpty = rAttributes.getDisplayEmptyCellsAs(); + if (eDisplayEmpty == sc::DisplayEmptyCellsAs::Gap) + eAction = SparklineValue::Action::Skip; + else if (eDisplayEmpty == sc::DisplayEmptyCellsAs::Span) + eAction = SparklineValue::Action::Interpolate; + } + else + { + fCellValue = mrDocument.GetValue(rCurrent); + } + + aSparklineValues.add(fCellValue, eAction); + } + + if (rAttributes.isRightToLeft()) + aSparklineValues.reverse(); + + if (rAttributes.getType() == sc::SparklineType::Column) + { + drawColumn(rRenderContext, aOutputRectangle, aSparklineValues, + pSparklineGroup->getAttributes()); + } + else if (rAttributes.getType() == sc::SparklineType::Stacked) + { + aSparklineValues.convertToStacked(); + drawColumn(rRenderContext, aOutputRectangle, aSparklineValues, + pSparklineGroup->getAttributes()); + } + else if (rAttributes.getType() == sc::SparklineType::Line) + { + drawLine(rRenderContext, aOutputRectangle, aSparklineValues, + pSparklineGroup->getAttributes()); + } + } +}; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |