/* -*- 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 #include #include #include #include #include #include #include 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::max(); std::vector maToInterpolateIndex; std::vector maValueList; public: size_t mnFirstIndex = std::numeric_limits::max(); size_t mnLastIndex = 0; double mfMinimum = std::numeric_limits::max(); double mfMaximum = std::numeric_limits::min(); std::vector 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::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& 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 const& rValueList = rSparklineValues.getValuesList(); std::vector aPolygons; aPolygons.emplace_back(); double numebrOfSteps = rValueList.size() - 1; double xStep = 0; double nDelta = nMax - nMin; std::vector 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 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 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: */