| 1 | // Copyright (C) 2023 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include <QtQuick/private/qquickrectangle_p.h> |
| 5 | #include <QtQuick/private/qquicktaphandler_p.h> |
| 6 | #include <QtGraphs/qbarseries.h> |
| 7 | #include <QtGraphs/qbarset.h> |
| 8 | #include <private/barsrenderer_p.h> |
| 9 | #include <private/axisrenderer_p.h> |
| 10 | #include <private/qbarseries_p.h> |
| 11 | #include <private/qgraphsview_p.h> |
| 12 | |
| 13 | QT_BEGIN_NAMESPACE |
| 14 | |
| 15 | static const char* TAG_BAR_COLOR = "barColor" ; |
| 16 | static const char* TAG_BAR_BORDER_COLOR = "barBorderColor" ; |
| 17 | static const char* TAG_BAR_BORDER_WIDTH = "barBorderWidth" ; |
| 18 | static const char* TAG_BAR_SELECTED = "barSelected" ; |
| 19 | static const char* TAG_BAR_VALUE = "barValue" ; |
| 20 | static const char* TAG_BAR_LABEL = "barLabel" ; |
| 21 | static const char* TAG_BAR_INDEX = "barIndex" ; |
| 22 | |
| 23 | BarsRenderer::BarsRenderer(QGraphsView *graph, bool clipPlotArea) |
| 24 | : QQuickItem(graph) |
| 25 | , m_graph(graph) |
| 26 | { |
| 27 | setFlag(flag: QQuickItem::ItemHasContents); |
| 28 | setClip(clipPlotArea); |
| 29 | |
| 30 | m_tapHandler = new QQuickTapHandler(this); |
| 31 | connect(sender: m_tapHandler, signal: &QQuickTapHandler::singleTapped, context: this, slot: &BarsRenderer::onSingleTapped); |
| 32 | connect(sender: m_tapHandler, signal: &QQuickTapHandler::doubleTapped, context: this, slot: &BarsRenderer::onDoubleTapped); |
| 33 | connect(sender: m_tapHandler, signal: &QQuickTapHandler::pressedChanged, context: this, slot: &BarsRenderer::onPressedChanged); |
| 34 | } |
| 35 | |
| 36 | BarsRenderer::~BarsRenderer() {} |
| 37 | |
| 38 | // Returns color in this order: |
| 39 | // 1) QBarSet::color if that is defined (alpha > 0). |
| 40 | // 2) QBarSeries::seriesColors at index if that is defined. |
| 41 | // 3) QGraphsTheme::seriesColors at index. |
| 42 | // 4) Black if seriesColors is empty. |
| 43 | QColor BarsRenderer::getSetColor(QBarSeries *series, QBarSet *set, qsizetype barSeriesIndex) |
| 44 | { |
| 45 | const auto &seriesColors = !series->seriesColors().isEmpty() |
| 46 | ? series->seriesColors() : m_graph->theme()->seriesColors(); |
| 47 | if (seriesColors.isEmpty()) |
| 48 | return QColorConstants::Black; |
| 49 | qsizetype index = m_colorIndex + barSeriesIndex; |
| 50 | index = index % seriesColors.size(); |
| 51 | QColor color = set->color().alpha() != 0 |
| 52 | ? set->color() |
| 53 | : seriesColors.at(i: index); |
| 54 | return color; |
| 55 | } |
| 56 | |
| 57 | QColor BarsRenderer::getSetSelectedColor(QBarSeries *series, QBarSet *set) |
| 58 | { |
| 59 | Q_UNUSED(series); |
| 60 | auto theme = m_graph->theme(); |
| 61 | QColor color = set->selectedColor().alpha() != 0 |
| 62 | ? set->selectedColor() |
| 63 | : theme->singleHighlightColor(); |
| 64 | return color; |
| 65 | } |
| 66 | |
| 67 | QColor BarsRenderer::getSetBorderColor(QBarSeries *series, QBarSet *set, qsizetype barSeriesIndex) |
| 68 | { |
| 69 | const auto &borderColors = !series->borderColors().isEmpty() |
| 70 | ? series->borderColors() : m_graph->theme()->borderColors(); |
| 71 | if (borderColors.isEmpty()) |
| 72 | return QColorConstants::Black; |
| 73 | qsizetype index = m_colorIndex + barSeriesIndex; |
| 74 | index = index % borderColors.size(); |
| 75 | QColor color = set->borderColor().alpha() != 0 |
| 76 | ? set->borderColor() |
| 77 | : borderColors.at(i: index); |
| 78 | return color; |
| 79 | } |
| 80 | |
| 81 | qreal BarsRenderer::getSetBorderWidth(QBarSeries *series, QBarSet *set) |
| 82 | { |
| 83 | Q_UNUSED(series); |
| 84 | auto theme = m_graph->theme(); |
| 85 | qreal borderWidth = set->borderWidth(); |
| 86 | if (qFuzzyCompare(p1: borderWidth, p2: qreal(-1.0))) |
| 87 | borderWidth = theme->borderWidth(); |
| 88 | return borderWidth; |
| 89 | } |
| 90 | |
| 91 | QString BarsRenderer::generateLabelText(QBarSeries *series, qreal value) |
| 92 | { |
| 93 | static const QString valueTag(QLatin1String("@value" )); |
| 94 | QString valueString = QString::number(value, format: 'f', precision: series->labelsPrecision()); |
| 95 | QString valueLabel; |
| 96 | if (series->labelsFormat().isEmpty()) { |
| 97 | valueLabel = valueString; |
| 98 | } else { |
| 99 | valueLabel = series->labelsFormat(); |
| 100 | valueLabel.replace(before: valueTag, after: valueString); |
| 101 | } |
| 102 | return valueLabel; |
| 103 | } |
| 104 | |
| 105 | void BarsRenderer::positionLabelItem(QBarSeries *series, QQuickText *textItem, const BarSeriesData &d) |
| 106 | { |
| 107 | auto pos = series->labelsPosition(); |
| 108 | const bool vertical = m_graph->orientation() == Qt::Orientation::Vertical; |
| 109 | const float w = textItem->contentWidth() + series->labelsMargin() * 2; |
| 110 | const float h = textItem->contentHeight() + series->labelsMargin() * 2; |
| 111 | textItem->setWidth(w); |
| 112 | textItem->setHeight(h); |
| 113 | textItem->setHAlign(QQuickText::HAlignment::AlignHCenter); |
| 114 | textItem->setVAlign(QQuickText::VAlignment::AlignVCenter); |
| 115 | if (pos == QBarSeries::LabelsPosition::Center) { |
| 116 | textItem->setX(d.rect.x() + d.rect.width() * 0.5 - w * 0.5); |
| 117 | textItem->setY(d.rect.y() + d.rect.height() * 0.5 - h * 0.5); |
| 118 | } else if (pos == QBarSeries::LabelsPosition::InsideEnd) { |
| 119 | if (vertical) { |
| 120 | textItem->setX(d.rect.x() + d.rect.width() * 0.5 - w * 0.5); |
| 121 | textItem->setY(d.rect.y()); |
| 122 | } else { |
| 123 | textItem->setX(d.rect.x() + d.rect.width() - w); |
| 124 | textItem->setY(d.rect.y() + d.rect.height() * 0.5 - h * 0.5); |
| 125 | } |
| 126 | } else if (pos == QBarSeries::LabelsPosition::InsideBase) { |
| 127 | if (vertical) { |
| 128 | textItem->setX(d.rect.x() + d.rect.width() * 0.5 - w * 0.5); |
| 129 | textItem->setY(d.rect.y() + d.rect.height() - h); |
| 130 | } else { |
| 131 | textItem->setX(d.rect.x()); |
| 132 | textItem->setY(d.rect.y() + d.rect.height() * 0.5 - h * 0.5); |
| 133 | } |
| 134 | } else { |
| 135 | // OutsideEnd |
| 136 | if (vertical) { |
| 137 | textItem->setX(d.rect.x() + d.rect.width() * 0.5 - w * 0.5); |
| 138 | textItem->setY(d.rect.y() - h); |
| 139 | } else { |
| 140 | textItem->setX(d.rect.x() + d.rect.width()); |
| 141 | textItem->setY(d.rect.y() + d.rect.height() * 0.5 - h * 0.5); |
| 142 | } |
| 143 | } |
| 144 | textItem->update(); |
| 145 | } |
| 146 | |
| 147 | void BarsRenderer::updateComponents(QBarSeries *series) |
| 148 | { |
| 149 | int barIndex = 0; |
| 150 | auto &seriesData = m_seriesData[series]; |
| 151 | auto &barItems = m_barItems[series]; |
| 152 | for (auto i = seriesData.cbegin(), end = seriesData.cend(); i != end; ++i) { |
| 153 | if (barItems.size() <= barIndex) { |
| 154 | QQuickItem *item = nullptr; |
| 155 | // Create more components as needed |
| 156 | if (series->barDelegate()) { |
| 157 | item = qobject_cast<QQuickItem *>( |
| 158 | o: series->barDelegate()->create(context: series->barDelegate()->creationContext())); |
| 159 | } |
| 160 | if (!item) |
| 161 | item = new QQuickRectangle(); |
| 162 | item->setParent(this); |
| 163 | item->setParentItem(this); |
| 164 | barItems << item; |
| 165 | } |
| 166 | if (barItems.size() > barIndex) { |
| 167 | BarSeriesData d = *i; |
| 168 | if (series->barDelegate()) { |
| 169 | // Set custom bar components |
| 170 | auto &barItem = barItems[barIndex]; |
| 171 | barItem->setX(d.rect.x()); |
| 172 | barItem->setY(d.rect.y()); |
| 173 | barItem->setZ(series->zValue()); |
| 174 | barItem->setWidth(d.rect.width()); |
| 175 | barItem->setHeight(d.rect.height()); |
| 176 | barItem->setVisible(series->isVisible()); |
| 177 | // Check for specific dynamic properties |
| 178 | if (barItem->property(name: TAG_BAR_COLOR).isValid()) |
| 179 | barItem->setProperty(name: TAG_BAR_COLOR, value: d.color); |
| 180 | if (barItem->property(name: TAG_BAR_BORDER_COLOR).isValid()) |
| 181 | barItem->setProperty(name: TAG_BAR_BORDER_COLOR, value: d.borderColor); |
| 182 | if (barItem->property(name: TAG_BAR_BORDER_WIDTH).isValid()) |
| 183 | barItem->setProperty(name: TAG_BAR_BORDER_WIDTH, value: d.borderWidth); |
| 184 | if (barItem->property(name: TAG_BAR_SELECTED).isValid()) |
| 185 | barItem->setProperty(name: TAG_BAR_SELECTED, value: d.isSelected); |
| 186 | if (barItem->property(name: TAG_BAR_VALUE).isValid()) |
| 187 | barItem->setProperty(name: TAG_BAR_VALUE, value: d.value); |
| 188 | if (barItem->property(name: TAG_BAR_LABEL).isValid()) |
| 189 | barItem->setProperty(name: TAG_BAR_LABEL, value: d.label); |
| 190 | if (barItem->property(name: TAG_BAR_INDEX).isValid()) |
| 191 | barItem->setProperty(name: TAG_BAR_INDEX, value: barIndex); |
| 192 | } else { |
| 193 | // Set default rectangle bars |
| 194 | auto barItem = qobject_cast<QQuickRectangle *>(object: barItems[barIndex]); |
| 195 | if (barItem) { |
| 196 | barItem->setX(d.rect.x()); |
| 197 | barItem->setY(d.rect.y()); |
| 198 | barItem->setZ(series->zValue()); |
| 199 | barItem->setWidth(d.rect.width()); |
| 200 | barItem->setHeight(d.rect.height()); |
| 201 | barItem->setVisible(series->isVisible()); |
| 202 | barItem->setColor(d.color); |
| 203 | barItem->border()->setColor(d.borderColor); |
| 204 | barItem->border()->setWidth(d.borderWidth); |
| 205 | barItem->setRadius(4.0); |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | barIndex++; |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | void BarsRenderer::updateValueLabels(QBarSeries *series) |
| 214 | { |
| 215 | if (!series->barDelegate() && series->isVisible() && series->labelsVisible()) { |
| 216 | // Update default value labels |
| 217 | int barIndex = 0; |
| 218 | auto &seriesData = m_seriesData[series]; |
| 219 | auto &labelTextItems = m_labelTextItems[series]; |
| 220 | for (auto i = seriesData.cbegin(), end = seriesData.cend(); i != end; ++i) { |
| 221 | if (labelTextItems.size() <= barIndex) { |
| 222 | // Create more label items as needed |
| 223 | auto labelItem = new QQuickText(this); |
| 224 | labelTextItems << labelItem; |
| 225 | } |
| 226 | if (labelTextItems.size() > barIndex) { |
| 227 | // Set label item values |
| 228 | auto &textItem = labelTextItems[barIndex]; |
| 229 | const auto d = *i; |
| 230 | if (qFuzzyIsNull(f: d.value)) { |
| 231 | textItem->setVisible(false); |
| 232 | } else { |
| 233 | textItem->setVisible(series->labelsVisible()); |
| 234 | QString valueLabel = generateLabelText(series, value: d.value); |
| 235 | textItem->setText(valueLabel); |
| 236 | positionLabelItem(series, textItem, d); |
| 237 | QColor labelColor = d.labelColor; |
| 238 | if (labelColor.alpha() == 0) { |
| 239 | // TODO: Use graphs theme labels color. |
| 240 | labelColor = QColor(255, 255, 255); |
| 241 | } |
| 242 | textItem->setColor(labelColor); |
| 243 | textItem->setRotation(series->labelsAngle()); |
| 244 | } |
| 245 | } |
| 246 | barIndex++; |
| 247 | } |
| 248 | } else { |
| 249 | // Hide all possibly existing label items |
| 250 | auto &labelTextItems = m_labelTextItems[series]; |
| 251 | for (auto textItem : labelTextItems) |
| 252 | textItem->setVisible(false); |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | void calculateCategoryTotalValues(QBarSeries *series, QList<float> &totalValues, qsizetype valuesPerSet) |
| 257 | { |
| 258 | totalValues.fill(t: 0, newSize: valuesPerSet); |
| 259 | auto barsets = series->barSets(); |
| 260 | for (auto s : std::as_const(t&: barsets)) { |
| 261 | QVariantList v = s->values(); |
| 262 | int setIndex = 0; |
| 263 | for (const auto &variantValue : std::as_const(t&: v)) { |
| 264 | if (setIndex < totalValues.size()) |
| 265 | totalValues[setIndex] += variantValue.toReal(); |
| 266 | setIndex++; |
| 267 | } |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | void BarsRenderer::updateVerticalBars(QBarSeries *series, qsizetype setCount, qsizetype valuesPerSet) |
| 272 | { |
| 273 | bool stacked = series->barsType() == QBarSeries::BarsType::Stacked |
| 274 | || series->barsType() == QBarSeries::BarsType::StackedPercent; |
| 275 | bool percent = series->barsType() == QBarSeries::BarsType::StackedPercent; |
| 276 | // Bars area width & height |
| 277 | float w = width(); |
| 278 | float h = height(); |
| 279 | // Max width of a bar if no separation between sets. |
| 280 | float maxBarWidth = w / (setCount * valuesPerSet) - m_barMargin; |
| 281 | if (stacked) |
| 282 | maxBarWidth = w / valuesPerSet; |
| 283 | // Actual bar width. |
| 284 | float barWidth = maxBarWidth * series->barWidth(); |
| 285 | // Helper to keep barsets centered when bar width is less than max width. |
| 286 | float barCentering = (maxBarWidth - barWidth) * setCount * 0.5; |
| 287 | if (stacked) |
| 288 | barCentering = (maxBarWidth - barWidth) * 0.5; |
| 289 | |
| 290 | auto &seriesData = m_seriesData[series]; |
| 291 | auto &rectNodesInputRects = m_rectNodesInputRects[series]; |
| 292 | // Clear the selection rects |
| 293 | // These will be filled only if series is selectable |
| 294 | rectNodesInputRects.clear(); |
| 295 | seriesData.clear(); |
| 296 | |
| 297 | float seriesPos = 0; |
| 298 | float posXInSet = 0; |
| 299 | QList<float> posYListInSet; |
| 300 | if (stacked) |
| 301 | posYListInSet.fill(t: 0, newSize: valuesPerSet); |
| 302 | QList<float> totalValuesListInSet; |
| 303 | if (percent) |
| 304 | calculateCategoryTotalValues(series, totalValues&: totalValuesListInSet, valuesPerSet); |
| 305 | |
| 306 | int barIndexInSet = 0; |
| 307 | int barSeriesIndex = 0; |
| 308 | QList<QLegendData> legendDataList; |
| 309 | auto barsets = series->barSets(); |
| 310 | for (auto s : std::as_const(t&: barsets)) { |
| 311 | QVariantList v = s->values(); |
| 312 | qsizetype valuesCount = v.size(); |
| 313 | if (valuesCount == 0) |
| 314 | continue; |
| 315 | seriesPos = 0; |
| 316 | barIndexInSet = 0; |
| 317 | BarSelectionRect *barSelectionRect = nullptr; |
| 318 | if (series->isSelectable() || series->isHoverable()) { |
| 319 | rectNodesInputRects << BarSelectionRect(); |
| 320 | barSelectionRect = &rectNodesInputRects.last(); |
| 321 | barSelectionRect->barSet = s; |
| 322 | barSelectionRect->series = series; |
| 323 | } |
| 324 | |
| 325 | auto &axisY = m_graph->m_axisRenderer->getAxisY(series); |
| 326 | |
| 327 | QColor color = getSetColor(series, set: s, barSeriesIndex); |
| 328 | QColor borderColor = getSetBorderColor(series, set: s, barSeriesIndex); |
| 329 | qreal borderWidth = getSetBorderWidth(series, set: s); |
| 330 | |
| 331 | // Update legendData |
| 332 | legendDataList.push_back(t: { |
| 333 | .color: color, |
| 334 | .borderColor: borderColor, |
| 335 | .label: s->label() |
| 336 | }); |
| 337 | // Apply series opacity |
| 338 | color.setAlpha(color.alpha() * series->opacity()); |
| 339 | borderColor.setAlpha(borderColor.alpha() * series->opacity()); |
| 340 | const auto selectedBars = s->selectedBars(); |
| 341 | for (const auto &variantValue : std::as_const(t&: v)) { |
| 342 | const float realValue = variantValue.toReal(); |
| 343 | float value = (realValue - axisY.minValue) * series->valuesMultiplier(); |
| 344 | if (percent) { |
| 345 | if (auto totalValue = totalValuesListInSet.at(i: barIndexInSet)) |
| 346 | value *= (100.0 / totalValue); |
| 347 | } |
| 348 | const bool isSelected = selectedBars.contains(t: barIndexInSet); |
| 349 | double delta = axisY.maxValue - axisY.minValue; |
| 350 | double maxValues = delta > 0 ? 1.0 / delta : 100.0; |
| 351 | float barLength = h * value * maxValues; |
| 352 | float barY = h - barLength; |
| 353 | float barX = seriesPos + posXInSet + barCentering; |
| 354 | if (stacked) { |
| 355 | barY = h - barLength - posYListInSet[barIndexInSet]; |
| 356 | barX = seriesPos + barCentering; |
| 357 | } |
| 358 | QRectF barRect(barX, barY, barWidth, barLength); |
| 359 | if (barSelectionRect) |
| 360 | barSelectionRect->rects << barRect; |
| 361 | |
| 362 | // Collect the series data |
| 363 | BarSeriesData d; |
| 364 | d.rect = barRect; |
| 365 | d.color = isSelected ? getSetSelectedColor(series, set: s) : color; |
| 366 | d.borderColor = borderColor; |
| 367 | d.borderWidth = borderWidth; |
| 368 | d.isSelected = isSelected; |
| 369 | d.label = s->label(); |
| 370 | d.labelColor = s->labelColor(); |
| 371 | d.value = realValue; |
| 372 | seriesData << d; |
| 373 | |
| 374 | if (stacked) |
| 375 | posYListInSet[barIndexInSet] += barLength; |
| 376 | barIndexInSet++; |
| 377 | seriesPos = ((float)barIndexInSet / valuesPerSet) * w; |
| 378 | } |
| 379 | posXInSet += barWidth + m_barMargin; |
| 380 | barSeriesIndex++; |
| 381 | } |
| 382 | series->d_func()->setLegendData(legendDataList); |
| 383 | } |
| 384 | |
| 385 | void BarsRenderer::updateHorizontalBars(QBarSeries *series, qsizetype setCount, qsizetype valuesPerSet) |
| 386 | { |
| 387 | bool stacked = series->barsType() == QBarSeries::BarsType::Stacked |
| 388 | || series->barsType() == QBarSeries::BarsType::StackedPercent; |
| 389 | bool percent = series->barsType() == QBarSeries::BarsType::StackedPercent; |
| 390 | // Bars area width & height |
| 391 | float w = width(); |
| 392 | float h = height(); |
| 393 | // Max width of a bar if no separation between sets. |
| 394 | float maxBarWidth = h / (setCount * valuesPerSet) - m_barMargin; |
| 395 | if (stacked) |
| 396 | maxBarWidth = h / valuesPerSet; |
| 397 | // Actual bar width. |
| 398 | float barWidth = maxBarWidth * series->barWidth(); |
| 399 | // Helper to keep barsets centered when bar width is less than max width. |
| 400 | float barCentering = (maxBarWidth - barWidth) * setCount * 0.5; |
| 401 | if (stacked) |
| 402 | barCentering = (maxBarWidth - barWidth) * 0.5; |
| 403 | |
| 404 | auto &seriesData = m_seriesData[series]; |
| 405 | auto &rectNodesInputRects = m_rectNodesInputRects[series]; |
| 406 | // Clear the selection rects |
| 407 | // These will be filled only if series is selectable |
| 408 | rectNodesInputRects.clear(); |
| 409 | seriesData.clear(); |
| 410 | |
| 411 | float seriesPos = 0; |
| 412 | float posYInSet = 0; |
| 413 | QList<float> posXListInSet; |
| 414 | if (stacked) |
| 415 | posXListInSet.fill(t: 0, newSize: valuesPerSet); |
| 416 | QList<float> totalValuesListInSet; |
| 417 | if (percent) |
| 418 | calculateCategoryTotalValues(series, totalValues&: totalValuesListInSet, valuesPerSet); |
| 419 | int barIndexInSet = 0; |
| 420 | int barSerieIndex = 0; |
| 421 | QList<QLegendData> legendDataList; |
| 422 | auto barsets = series->barSets(); |
| 423 | for (auto s : std::as_const(t&: barsets)) { |
| 424 | QVariantList v = s->values(); |
| 425 | qsizetype valuesCount = v.size(); |
| 426 | if (valuesCount == 0) |
| 427 | continue; |
| 428 | seriesPos = 0; |
| 429 | barIndexInSet = 0; |
| 430 | BarSelectionRect *barSelectionRect = nullptr; |
| 431 | if (series->isSelectable() || series->isHoverable()) { |
| 432 | rectNodesInputRects << BarSelectionRect(); |
| 433 | barSelectionRect = &rectNodesInputRects.last(); |
| 434 | barSelectionRect->barSet = s; |
| 435 | barSelectionRect->series = series; |
| 436 | } |
| 437 | |
| 438 | auto &axisX = m_graph->m_axisRenderer->getAxisX(series); |
| 439 | |
| 440 | QColor color = getSetColor(series, set: s, barSeriesIndex: barSerieIndex); |
| 441 | QColor borderColor = getSetBorderColor(series, set: s, barSeriesIndex: barSerieIndex); |
| 442 | qreal borderWidth = getSetBorderWidth(series, set: s); |
| 443 | // Update legendData |
| 444 | legendDataList.push_back(t: { |
| 445 | .color: color, |
| 446 | .borderColor: borderColor, |
| 447 | .label: s->label() |
| 448 | }); |
| 449 | // Apply series opacity |
| 450 | color.setAlpha(color.alpha() * series->opacity()); |
| 451 | borderColor.setAlpha(borderColor.alpha() * series->opacity()); |
| 452 | const auto selectedBars = s->selectedBars(); |
| 453 | for (const auto &variantValue : std::as_const(t&: v)) { |
| 454 | const float realValue = variantValue.toReal(); |
| 455 | float value = (realValue - axisX.minValue) * series->valuesMultiplier(); |
| 456 | if (percent) { |
| 457 | if (auto totalValue = totalValuesListInSet.at(i: barIndexInSet)) |
| 458 | value *= (100.0 / totalValue); |
| 459 | } |
| 460 | const bool isSelected = selectedBars.contains(t: barIndexInSet); |
| 461 | double delta = axisX.maxValue - axisX.minValue; |
| 462 | double maxValues = delta > 0 ? 1.0 / delta : 100.0; |
| 463 | float barLength = w * value * maxValues; |
| 464 | float barY = seriesPos + posYInSet + barCentering; |
| 465 | float barX = 0; |
| 466 | if (stacked) { |
| 467 | barY = seriesPos + barCentering; |
| 468 | barX = posXListInSet[barIndexInSet]; |
| 469 | } |
| 470 | QRectF barRect(barX, barY, barLength, barWidth); |
| 471 | if (barSelectionRect) |
| 472 | barSelectionRect->rects << barRect; |
| 473 | |
| 474 | // Collect the series data |
| 475 | BarSeriesData d; |
| 476 | d.rect = barRect; |
| 477 | d.color = isSelected ? getSetSelectedColor(series, set: s) : color; |
| 478 | d.borderColor = borderColor; |
| 479 | d.borderWidth = borderWidth; |
| 480 | d.isSelected = isSelected; |
| 481 | d.label = s->label(); |
| 482 | d.labelColor = s->labelColor(); |
| 483 | d.value = realValue; |
| 484 | seriesData << d; |
| 485 | |
| 486 | if (stacked) |
| 487 | posXListInSet[barIndexInSet] += barLength; |
| 488 | barIndexInSet++; |
| 489 | seriesPos = ((float)barIndexInSet / valuesPerSet) * h; |
| 490 | } |
| 491 | posYInSet += barWidth + m_barMargin; |
| 492 | barSerieIndex++; |
| 493 | } |
| 494 | series->d_func()->setLegendData(legendDataList); |
| 495 | } |
| 496 | |
| 497 | void BarsRenderer::handlePolish(QBarSeries *series) |
| 498 | { |
| 499 | auto theme = m_graph->theme(); |
| 500 | if (!theme) { |
| 501 | qCCritical(lcCritical2D, "Theme not found." ); |
| 502 | return; |
| 503 | } |
| 504 | |
| 505 | if (!m_graph->m_axisRenderer) { |
| 506 | qCCritical(lcCritical2D, "Axis renderer not found." ); |
| 507 | return; |
| 508 | } |
| 509 | |
| 510 | qsizetype setCount = series->barSets().size(); |
| 511 | auto &seriesData = m_seriesData[series]; |
| 512 | auto &barItems = m_barItems[series]; |
| 513 | auto &rectNodesInputRects = m_rectNodesInputRects[series]; |
| 514 | if (setCount == 0) { |
| 515 | for (int i = 0; i < barItems.size(); i++) |
| 516 | barItems[i]->deleteLater(); |
| 517 | barItems.clear(); |
| 518 | |
| 519 | series->d_func()->clearLegendData(); |
| 520 | rectNodesInputRects.clear(); |
| 521 | seriesData.clear(); |
| 522 | return; |
| 523 | } |
| 524 | |
| 525 | if (m_colorIndex < 0) |
| 526 | m_colorIndex = m_graph->graphSeriesCount(); |
| 527 | m_graph->setGraphSeriesCount(m_colorIndex + setCount); |
| 528 | |
| 529 | if (series->barDelegateDirty() && !barItems.isEmpty()) { |
| 530 | // Bars delegate has changed, so remove the old items. |
| 531 | for (int i = 0; i < barItems.size(); i++) |
| 532 | barItems[i]->deleteLater(); |
| 533 | barItems.clear(); |
| 534 | series->setBarDelegateDirty(false); |
| 535 | } |
| 536 | |
| 537 | // Get bars values |
| 538 | qsizetype valuesPerSet = series->barSets().constFirst()->values().size(); |
| 539 | if (m_graph->orientation() == Qt::Orientation::Vertical) |
| 540 | updateVerticalBars(series, setCount, valuesPerSet); |
| 541 | else |
| 542 | updateHorizontalBars(series, setCount, valuesPerSet); |
| 543 | updateComponents(series); |
| 544 | updateValueLabels(series); |
| 545 | |
| 546 | // Remove additional components |
| 547 | for (qsizetype i = barItems.size() - 1; i >= seriesData.size(); --i) |
| 548 | barItems[i]->deleteLater(); |
| 549 | const auto range = barItems.size() - seriesData.size(); |
| 550 | if (range > 0) |
| 551 | barItems.remove(i: seriesData.size(), n: range); |
| 552 | } |
| 553 | |
| 554 | void BarsRenderer::updateSeries(QBarSeries *series) |
| 555 | { |
| 556 | Q_UNUSED(series); |
| 557 | } |
| 558 | |
| 559 | void BarsRenderer::afterUpdate(QList<QAbstractSeries *> &cleanupSeries) |
| 560 | { |
| 561 | Q_UNUSED(cleanupSeries); |
| 562 | } |
| 563 | |
| 564 | void BarsRenderer::afterPolish(QList<QAbstractSeries *> &cleanupSeries) |
| 565 | { |
| 566 | for (auto &cleanupSerie : cleanupSeries) { |
| 567 | auto series = qobject_cast<QBarSeries *>(object: cleanupSerie); |
| 568 | if (series && m_barItems.contains(key: series)) { |
| 569 | // Remove custom bar items |
| 570 | auto &barItems = m_barItems[series]; |
| 571 | for (int i = 0; i < barItems.size(); i++) |
| 572 | barItems[i]->deleteLater(); |
| 573 | barItems.clear(); |
| 574 | m_barItems.remove(key: series); |
| 575 | } |
| 576 | if (series && m_labelTextItems.contains(key: series)) { |
| 577 | // Remove bar label items |
| 578 | auto &labelTextItems = m_labelTextItems[series]; |
| 579 | for (int i = 0; i < labelTextItems.size(); i++) |
| 580 | labelTextItems[i]->deleteLater(); |
| 581 | labelTextItems.clear(); |
| 582 | m_labelTextItems.remove(key: series); |
| 583 | } |
| 584 | } |
| 585 | } |
| 586 | |
| 587 | bool BarsRenderer::handleHoverMove(QHoverEvent *event) |
| 588 | { |
| 589 | bool handled = false; |
| 590 | const QPointF &position = event->position(); |
| 591 | |
| 592 | bool hovering = false; |
| 593 | for (auto &rectNodesInputRects : m_rectNodesInputRects) { |
| 594 | for (auto &barSelection : rectNodesInputRects) { |
| 595 | int indexInSet = 0; |
| 596 | |
| 597 | for (auto &rect : barSelection.rects) { |
| 598 | if (rect.contains(p: event->position().toPoint())) { |
| 599 | const QString &name = barSelection.series->name(); |
| 600 | const QPointF point(indexInSet, barSelection.barSet->at(index: indexInSet)); |
| 601 | |
| 602 | if (!m_currentHoverSeries) { |
| 603 | m_currentHoverSeries = barSelection.series; |
| 604 | barSelection.series->setHovered(true); |
| 605 | emit barSelection.series->hoverEnter(seriesName: name, position, value: point); |
| 606 | } |
| 607 | |
| 608 | emit barSelection.series->hover(seriesName: name, position, value: point); |
| 609 | hovering = true; |
| 610 | handled = true; |
| 611 | } |
| 612 | indexInSet++; |
| 613 | } |
| 614 | } |
| 615 | } |
| 616 | |
| 617 | if (!hovering && m_currentHoverSeries) { |
| 618 | m_currentHoverSeries->setHovered(false); |
| 619 | emit m_currentHoverSeries->hoverExit(seriesName: m_currentHoverSeries->name(), position); |
| 620 | m_currentHoverSeries = nullptr; |
| 621 | handled = true; |
| 622 | } |
| 623 | return handled; |
| 624 | } |
| 625 | |
| 626 | void BarsRenderer::onSingleTapped(QEventPoint eventPoint, Qt::MouseButton button) |
| 627 | { |
| 628 | Q_UNUSED(button) |
| 629 | |
| 630 | for (auto &rectNodesInputRects : m_rectNodesInputRects) { |
| 631 | for (auto &barSelection : rectNodesInputRects) { |
| 632 | if (!barSelection.series->isSelectable()) |
| 633 | continue; |
| 634 | qsizetype indexInSet = 0; |
| 635 | for (auto &rect : barSelection.rects) { |
| 636 | if (rect.contains(p: eventPoint.position())) { |
| 637 | emit barSelection.series->clicked(index: indexInSet, barset: barSelection.barSet); |
| 638 | return; |
| 639 | } |
| 640 | indexInSet++; |
| 641 | } |
| 642 | } |
| 643 | } |
| 644 | } |
| 645 | |
| 646 | void BarsRenderer::onDoubleTapped(QEventPoint eventPoint, Qt::MouseButton button) |
| 647 | { |
| 648 | Q_UNUSED(button) |
| 649 | |
| 650 | for (auto &rectNodesInputRects : m_rectNodesInputRects) { |
| 651 | for (auto &barSelection : rectNodesInputRects) { |
| 652 | if (!barSelection.series->isSelectable()) |
| 653 | continue; |
| 654 | qsizetype indexInSet = 0; |
| 655 | for (auto &rect : barSelection.rects) { |
| 656 | if (rect.contains(p: eventPoint.position())) { |
| 657 | emit barSelection.series->doubleClicked(index: indexInSet, barset: barSelection.barSet); |
| 658 | return; |
| 659 | } |
| 660 | indexInSet++; |
| 661 | } |
| 662 | } |
| 663 | } |
| 664 | } |
| 665 | |
| 666 | void BarsRenderer::onPressedChanged() |
| 667 | { |
| 668 | for (auto &rectNodesInputRects : m_rectNodesInputRects) { |
| 669 | for (auto &barSelection : rectNodesInputRects) { |
| 670 | if (!barSelection.series->isSelectable()) |
| 671 | continue; |
| 672 | qsizetype indexInSet = 0; |
| 673 | for (auto &rect : barSelection.rects) { |
| 674 | if (rect.contains(p: m_tapHandler->point().position())) { |
| 675 | // TODO: Currently just toggling selection |
| 676 | if (m_tapHandler->isPressed()) { |
| 677 | QList<qsizetype> indexList = {indexInSet}; |
| 678 | barSelection.barSet->toggleSelection(indexes: indexList); |
| 679 | emit barSelection.series->pressed(index: indexInSet, barset: barSelection.barSet); |
| 680 | } else { |
| 681 | emit barSelection.series->released(index: indexInSet, barset: barSelection.barSet); |
| 682 | } |
| 683 | } |
| 684 | indexInSet++; |
| 685 | } |
| 686 | } |
| 687 | } |
| 688 | } |
| 689 | |
| 690 | QT_END_NAMESPACE |
| 691 | |