From bd7fe3c599b82cd3fdbfae27347ba2e741fee035 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Tue, 6 Feb 2024 17:27:52 +0800 Subject: [PATCH] Graph computeColors() new algorithm --- Meter.c | 454 +++++++++++++++++++++++++++++++++----------------------- Meter.h | 4 +- 2 files changed, 272 insertions(+), 186 deletions(-) diff --git a/Meter.c b/Meter.c index 3922feccb..625cba835 100644 --- a/Meter.c +++ b/Meter.c @@ -386,58 +386,104 @@ static uint8_t GraphMeterMode_findTopCellItem(const Meter* this, double scaledTo assert(topCell < graphHeight); double valueSum = 0.0; - double prevTopPoint = (double)(int)topCell; double maxArea = 0.0; uint8_t topCellItem = this->curItems - 1; for (uint8_t i = 0; i < this->curItems && valueSum < DBL_MAX; i++) { - if (!isPositive(this->values[i])) + double value = this->values[i]; + if (!isPositive(value)) continue; - valueSum += this->values[i]; - if (valueSum > DBL_MAX) - valueSum = DBL_MAX; + double newValueSum = valueSum + value; + if (newValueSum > DBL_MAX) + newValueSum = DBL_MAX; + + if (value > DBL_MAX - valueSum) { + value = DBL_MAX - valueSum; + assert(newValueSum < DBL_MAX || valueSum + value >= DBL_MAX); + } + + valueSum = newValueSum; double topPoint = (valueSum / scaledTotal) * (double)(int)graphHeight; - if (topPoint > prevTopPoint) { + double area = (value / scaledTotal) * (double)(int)graphHeight; + + if (topPoint - (double)(int)topCell > 0.0) { + if (area > topPoint - (double)(int)topCell) + area = topPoint - (double)(int)topCell; + // Find the item that occupies the largest area of the top cell. // Favor item with higher index in case of a tie. - if (topPoint - prevTopPoint >= maxArea) { + if (area >= maxArea) { topCellItem = i; - maxArea = topPoint - prevTopPoint; + maxArea = area; } - prevTopPoint = topPoint; } } return topCellItem; } -ATTR_UNUSED -static double GraphMeterMode_adjTopPoint(const GraphColorAdjStack* stack, double topPoint, uint32_t adjOffsetVal) { - if (stack->nItems == 0) - return topPoint; +static int GraphMeterMode_needsExtraCell(unsigned int graphHeight, double scaledTotal, unsigned int y, const GraphColorAdjStack* stack, const GraphColorAdjOffset* adjOffset) { + double areaSum = (stack->fractionSum + stack->valueSum / scaledTotal) * (double)(int)graphHeight; + double adjOffsetVal = adjOffset ? (double)(int32_t)adjOffset->offsetVal : 0.0; + double halfPoint = (double)(int)y + 0.5; + + // Calculate the best position for rendering this stack of items. + // The formula is arranged for minimizing rounding errors, so as to avoid + // tiebreaking whenever we can. + + // Given real numbers a, b, c and d (a <= b <= c <= d), then: + // 1. The smallest value for (x - a)^2 + (x - b)^2 + (x - c)^2 + (x - d)^2 + // happens when x == (a + b + c + d) / 4; x is the "arithmetic mean". + // 2. The smallest value for |y - a| + |y - b| + |y - c| + |y - d| + // happens when b <= y <= c; y is the "median". + // The arithmetic mean is used here. + + // The following variables are not needed for conditionals but mentioned + // for reference: + // double averagePoint = stack->startPoint + (areaSum / (stack->nItems * 2)); + // double adjStartPoint = stack->startPoint + ((areaSum - adjOffsetVal) / (stack->nItems * 2)); + + if (areaSum - adjOffsetVal > (halfPoint - stack->startPoint) * 2.0 * stack->nItems) { + // Mathematically equivalent to isgreater(adjStartPoint, halfPoint) + // except for rounding differences. + return 1; + } + + if (areaSum - adjOffsetVal < (halfPoint - stack->startPoint) * 2.0 * stack->nItems) + return 0; - return (topPoint + stack->pointsSum + (double)(int32_t)adjOffsetVal) / (stack->nItems * 2); + assert(stack->valueSum <= DBL_MAX); + double stackArea = (stack->valueSum / scaledTotal) * (double)(int)graphHeight; + double adjNCells = adjOffset ? (double)(int)adjOffset->nCells : 0.0; + + if (stackArea - adjNCells > (halfPoint - stack->startPoint) * 2.0) { + // Mathematically equivalent to: + // (stack->startPoint + stackArea / 2 > halfPoint + adjNCells / 2) + return 1; + } + + if (stackArea - adjNCells < (halfPoint - stack->startPoint) * 2.0) + return 0; + + return -1; } -ATTR_UNUSED -static void GraphMeterMode_addItemAdjOffset(GraphColorAdjOffset* adjOffset, unsigned int nCells, const GraphColorAdjStack* stack) { - adjOffset->offsetVal += nCells * ((uint32_t)stack->nItems * 2 + 1); +static void GraphMeterMode_addItemAdjOffset(GraphColorAdjOffset* adjOffset, unsigned int nCells) { + adjOffset->offsetVal += (uint32_t)adjOffset->nCells * 2 + nCells; adjOffset->nCells += nCells; } -ATTR_UNUSED -static void GraphMeterMode_addItemAdjStack(GraphColorAdjStack* stack, double topPoint) { - if (stack->nItems == 0) { - assert(stack->pointsSum <= 0.0); - stack->pointsSum = topPoint; - } else { - stack->pointsSum += topPoint * 2.0; - } +static void GraphMeterMode_addItemAdjStack(GraphColorAdjStack* stack, double scaledTotal, double value) { + assert(scaledTotal <= DBL_MAX); + assert(stack->valueSum < DBL_MAX); + + stack->fractionSum += (stack->valueSum / scaledTotal) * 2.0; + stack->valueSum += value; + assert(stack->nItems < UINT8_MAX); stack->nItems++; } -ATTR_UNUSED static uint16_t GraphMeterMode_makeDetailsMask(const GraphColorComputeState* prev, const GraphColorComputeState* new, double rem, int blanksAtTopCell) { assert(new->nCellsPainted > prev->nCellsPainted); assert(rem >= 0.0); @@ -445,7 +491,7 @@ static uint16_t GraphMeterMode_makeDetailsMask(const GraphColorComputeState* pre bool isItemOnEdge = (blanksAtTopCell >= 0 || prev->nCellsPainted == 0); const uint8_t dotAlignment = 2; - if (isItemOnEdge && rem < (0.125 * dotAlignment)) + if (isItemOnEdge && rem > 0.0 && rem < (0.125 * dotAlignment)) rem = (0.125 * dotAlignment); uint8_t blanksAtEnd; @@ -480,7 +526,7 @@ static uint16_t GraphMeterMode_makeDetailsMask(const GraphColorComputeState* pre uint8_t blanksAtStart; if (prev->nCellsPainted > 0) { - blanksAtStart = (uint8_t)((1.0 - rem) * 8.0) % 8 - blanksAtEnd; + blanksAtStart = (uint8_t)((int)((1.0 - rem) * 8.0) - blanksAtEnd) % 8; } else { // Always zero blanks for the first cell. // When an item would be painted with all cells (from the first cell to @@ -523,7 +569,6 @@ static uint16_t GraphMeterMode_makeDetailsMask(const GraphColorComputeState* pre return mask; } -ATTR_UNUSED static void GraphMeterMode_paintCellsForItem(GraphColorCell* cellsStart, unsigned int increment, uint8_t itemIndex, unsigned int nCells, uint16_t mask) { GraphColorCell* cell = cellsStart; while (nCells > 0) { @@ -540,219 +585,259 @@ static void GraphMeterMode_paintCellsForItem(GraphColorCell* cellsStart, unsigne } } -static void GraphMeterMode_computeColors(Meter* this, const GraphDrawContext* context, GraphColorCell* valueStart, int deltaExp, double scaledTotal, int numDots) { +static void GraphMeterMode_computeColors(Meter* this, const GraphDrawContext* context, GraphColorCell* valueStart, int deltaExp, double scaledTotal, unsigned int numDots) { unsigned int graphHeight = this->drawData.graphHeight; bool isPercentChart = context->isPercentChart; assert(deltaExp >= 0); - assert(numDots > 0 && numDots <= (int)graphHeight * 8); + assert(numDots > 0 && numDots <= graphHeight * 8); + + unsigned int increment; + unsigned int firstCellIndex = GraphMeterMode_valueCellIndex(graphHeight, isPercentChart, deltaExp, 0, NULL, &increment); - // If there is a "top cell" which will not be completely filled, determine - // its color first. - unsigned int topCell = ((unsigned int)numDots - 1) / 8; + unsigned int topCell = (numDots - 1) / 8; const uint8_t dotAlignment = 2; - unsigned int blanksAtTopCell = ((topCell + 1) * 8 - (unsigned int)numDots) / dotAlignment * dotAlignment; + unsigned int blanksAtTopCell = ((topCell + 1) * 8 - numDots) / dotAlignment * dotAlignment; bool hasPartialTopCell = false; if (blanksAtTopCell > 0) { hasPartialTopCell = true; - } else if (!isPercentChart && topCell == ((graphHeight - 1) >> deltaExp) && topCell % 2 == 0) { + } else if (!isPercentChart && topCell % 2 == 0 && topCell == ((graphHeight - 1) >> deltaExp)) { // This "top cell" is rendered as full in one scale, but partial in the // next scale. (Only happens when graphHeight is not a power of two.) hasPartialTopCell = true; } double topCellArea = 0.0; + assert(this->curItems > 0); uint8_t topCellItem = this->curItems - 1; if (hasPartialTopCell) { + // Allocate the "top cell" first. The item that acquires the "top cell" + // will have a smaller "area" for the remainder calculation below. topCellArea = (8 - (int)blanksAtTopCell) / 8.0; topCellItem = GraphMeterMode_findTopCellItem(this, scaledTotal, topCell); } - topCell += 1; // This index points to a cell that would be blank. - // NOTE: The algorithm below needs a rewrite. It's messy for now and don't expect the result would be accurate. - // BEGINNING OF THE PART THAT NEEDS REWRITING - - // Compute colors of the rest of the cells, using the largest remainder - // method (a.k.a. Hamilton's method). - // The Hare quota is (scaledTotal / graphHeight). - int paintedHigh = (int)topCell + (int)topCellItem + 1; - int paintedLow = 0; - double threshold = 0.5; + GraphColorComputeState restart = { + .valueSum = 0.0, + .topPoint = 0.0, + .nCellsPainted = 0, + .nItemsPainted = 0 + }; double thresholdHigh = 1.0; double thresholdLow = 0.0; - // Tiebreak 1: Favor items with less number of cells. (Top cell is not - // included in the count.) - int cellLimit = (int)topCell; - int cellLimitHigh = (int)topCell; - int cellLimitLow = 0.0; - // Tiebreak 2: Favor items whose indices are lower. - uint8_t tiedItemLimit = topCellItem; + double threshold = 0.5; + bool rItemIsDetermined = false; + bool rItemHasExtraCell = true; + unsigned int rItemMinCells = 0; + bool isLastTiebreak = false; + unsigned int nCellsToPaint = topCell + 1; + unsigned int nCellsPaintedHigh = nCellsToPaint + topCellItem + 1; + unsigned int nCellsPaintedLow = 0; + while (true) { - double sum = 0.0; - double bottom = 0.0; - int cellsPainted = 0; - double nextThresholdHigh = 0.0; - double nextThresholdLow = 1.0; - int nextCellLimitHigh = 0; - int nextCellLimitLow = (int)topCell; - uint8_t numTiedItems = 0; - for (uint8_t i = 0; i <= topCellItem && sum < DBL_MAX; i++) { - if (!isPositive(this->values[i])) + GraphColorComputeState prev = restart; + double nextThresholdHigh = thresholdLow; + double nextThresholdLow = thresholdHigh; + bool hasThresholdRange = thresholdLow < thresholdHigh; + GraphColorAdjOffset adjLarge = { + .offsetVal = 0, + .nCells = 0 + }; + GraphColorAdjOffset adjSmall = adjLarge; + GraphColorAdjStack stack = { + .startPoint = 0.0, + .fractionSum = 0.0, + .valueSum = 0.0, + .nItems = 0 + }; + + while (prev.nItemsPainted <= topCellItem && prev.valueSum < DBL_MAX) { + double value = this->values[prev.nItemsPainted]; + if (!isPositive(value)) { + if (restart.nItemsPainted == prev.nItemsPainted) { + restart.nItemsPainted++; + } + prev.nItemsPainted++; continue; + } - sum += this->values[i]; - if (sum > DBL_MAX) - sum = DBL_MAX; + GraphColorComputeState new; - double top = (sum / scaledTotal) * graphHeight; - double area = top - bottom; + new.valueSum = prev.valueSum + value; + if (new.valueSum > DBL_MAX) + new.valueSum = DBL_MAX; + + if (value > DBL_MAX - prev.valueSum) { + value = DBL_MAX - prev.valueSum; + // This assumption holds for the new "value" as long as the + // rounding mode is consistent. + assert(new.valueSum < DBL_MAX || prev.valueSum + value >= DBL_MAX); + } + + new.topPoint = (new.valueSum / scaledTotal) * (double)(int)graphHeight; + double area = (value / scaledTotal) * (double)(int)graphHeight; double rem = area; - if (i == topCellItem) { - rem -= topCellArea; - if (!(rem >= 0.0)) + + if (prev.nItemsPainted == topCellItem) + rem = MAXIMUM(area, topCellArea) - topCellArea; + + unsigned int nCells = (unsigned int)(int)rem; + rem -= (int)rem; + + // Whether the item will receive an extra cell or be truncated. + // The main method is known as the "largest remainder method". + + // An item whose remainder reaches the Droop quota may either receive + // an extra cell or need a tiebreak (a tie caused by rounding). + // This is the highest threshold we might need to compare with. + bool reachesDroopQuota = rem * (double)(int)(graphHeight + 1) > (double)(int)graphHeight; + if (reachesDroopQuota && rem < thresholdHigh) + thresholdHigh = rem; + + bool equalsThreshold = false; + bool isInThresholdRange = rem <= thresholdHigh && rem >= thresholdLow; + + if (rem > threshold) { + if (rem < nextThresholdLow) { + nextThresholdLow = rem; + } + nCells++; + } else if (rem < threshold) { + if (rem > nextThresholdHigh) { + nextThresholdHigh = rem; + } + rem = 0.0; + } else if (hasThresholdRange) { + assert(!rItemIsDetermined); + nCells++; + } else if (restart.nItemsPainted >= prev.nItemsPainted) { + assert(restart.nItemsPainted == prev.nItemsPainted); + + if (!rItemIsDetermined) { + stack.startPoint = new.topPoint; + rItemMinCells = nCells; rem = 0.0; - } - int numCells = (int)rem; - rem -= numCells; - - // Whether the item will receive an extra cell or be truncated - if (rem >= threshold) { - if (rem > threshold) { - if (rem < nextThresholdLow) - nextThresholdLow = rem; - numCells++; - } else if (numCells <= cellLimit) { - if (numCells < cellLimit) { - if (numCells > nextCellLimitHigh) - nextCellLimitHigh = numCells; - numCells++; - } else { - numTiedItems++; - if (numTiedItems <= tiedItemLimit) { - numCells++; - } else { - rem = 0.0; - } - } + } else if (rItemHasExtraCell) { + nCells++; } else { - if (numCells < nextCellLimitLow) - nextCellLimitLow = numCells; rem = 0.0; } } else { - if (rem > nextThresholdHigh) - nextThresholdHigh = rem; - rem = 0.0; - } + equalsThreshold = true; - // Paint cells to the buffer - uint8_t blanksAtEnd = 0; - if (i == topCellItem && topCellArea > 0.0) { - numCells++; - if (area < topCellArea) { - rem = MAXIMUM(area, 0.25); - blanksAtEnd = (uint8_t)blanksAtTopCell; + unsigned int y = restart.nCellsPainted + rItemMinCells; + + if (adjLarge.nCells > adjSmall.nCells) { + int res = GraphMeterMode_needsExtraCell(graphHeight, scaledTotal, y, &stack, &adjLarge); + + if (res == 1) { + rItemHasExtraCell = true; + break; + } + if (res == -1) { + if (rItemMinCells <= nCells) { + rItemHasExtraCell = true; + break; + } + } + } + + if (rItemHasExtraCell) { + int res = GraphMeterMode_needsExtraCell(graphHeight, scaledTotal, y, &stack, &adjSmall); + + if (res == 0) { + rItemHasExtraCell = false; + } else if (res == -1) { + if (rItemMinCells > nCells) { + rItemHasExtraCell = false; + } + } } - } else if (cellsPainted + numCells >= (int)topCell) { - blanksAtEnd = 0; - } else if (cellsPainted <= 0 || bottom <= cellsPainted) { - blanksAtEnd = ((uint8_t)((1.0 - rem) * 8.0) % 8); - } else if (cellsPainted + numCells > top) { - assert(cellsPainted + numCells - top < 1.0); - blanksAtEnd = (uint8_t)((cellsPainted + numCells - top) * 8.0); } - unsigned int blanksAtStart = 0; - if (cellsPainted > 0) { - blanksAtStart = ((uint8_t)((1.0 - rem) * 8.0) % 8 - blanksAtEnd); + if (!hasThresholdRange && restart.nItemsPainted < prev.nItemsPainted) { + GraphMeterMode_addItemAdjOffset(&adjLarge, nCells + equalsThreshold); + GraphMeterMode_addItemAdjOffset(&adjSmall, nCells); + GraphMeterMode_addItemAdjStack(&stack, scaledTotal, value); } - while (numCells > 0 && cellsPainted < (int)topCell) { - unsigned int offset = (unsigned int)cellsPainted; - if (!isPercentChart) { - offset = (offset * 2 + 1) << deltaExp; - } + if (hasPartialTopCell && prev.nItemsPainted == topCellItem) + nCells++; - valueStart[offset].c.itemIndex = (uint8_t)i; - valueStart[offset].c.details = 0xFF; + new.nCellsPainted = prev.nCellsPainted + nCells; + new.nItemsPainted = prev.nItemsPainted + 1; - if (blanksAtStart > 0) { - assert(blanksAtStart < 8); - valueStart[offset].c.details >>= blanksAtStart; - blanksAtStart = 0; + // Update the "restart" state if needed + if (restart.nItemsPainted >= prev.nItemsPainted) { + if (!isInThresholdRange) { + restart = new; + } else if (rItemIsDetermined) { + restart = new; + rItemIsDetermined = isLastTiebreak; + rItemHasExtraCell = true; } + } - if (cellsPainted == (int)topCell - 1) { - assert(blanksAtTopCell < 8); - valueStart[offset].c.details &= 0xFF << blanksAtTopCell; - } else if (numCells == 1) { - assert(blanksAtEnd < 8); - valueStart[offset].c.details &= 0xFF << blanksAtEnd; + // Paint cells to the buffer + if (hasPartialTopCell && prev.nItemsPainted == topCellItem) { + rem = area; + if (nCells == 1 && rem > topCellArea) { + rem = topCellArea; } + rem -= (int)rem; + } + + if (nCells > 0 && new.nCellsPainted <= nCellsToPaint) { + int blanksAtTopCellArg = (new.nCellsPainted == nCellsToPaint) ? (int)blanksAtTopCell : -1; + uint16_t mask = GraphMeterMode_makeDetailsMask(&prev, &new, rem, blanksAtTopCellArg); - numCells--; - cellsPainted++; + GraphColorCell* cellsStart = &valueStart[firstCellIndex + (size_t)increment * prev.nCellsPainted]; + GraphMeterMode_paintCellsForItem(cellsStart, increment, prev.nItemsPainted, nCells, mask); } - cellsPainted += numCells; - bottom = top; + prev = new; } - if (cellsPainted == (int)topCell) - break; + if (hasThresholdRange) { + if (prev.nCellsPainted == nCellsToPaint) + break; - // Set new bounds and threshold - if (cellsPainted > (int)topCell) { - paintedHigh = cellsPainted; - if (thresholdLow >= thresholdHigh) { - if (cellLimitLow >= cellLimitHigh) { - assert(tiedItemLimit >= topCellItem); - tiedItemLimit = numTiedItems - (uint8_t)(cellsPainted - (int)topCell); - } else { - assert(cellLimitHigh > cellLimit); - cellLimitHigh = cellLimit; - } - } else { + // Set new threshold range + if (prev.nCellsPainted > nCellsToPaint) { + nCellsPaintedHigh = prev.nCellsPainted; assert(thresholdLow < threshold); thresholdLow = threshold; - } - } else { - paintedLow = cellsPainted + 1; - if (thresholdLow >= thresholdHigh) { - assert(cellLimitLow < cellLimitHigh); - assert(cellLimitLow < nextCellLimitLow); - cellLimitLow = nextCellLimitLow; - nextCellLimitHigh = cellLimitHigh; } else { - assert(cellLimit >= (int)topCell); + nCellsPaintedLow = prev.nCellsPainted + 1; assert(thresholdHigh > nextThresholdHigh); thresholdHigh = nextThresholdHigh; nextThresholdLow = thresholdLow; } - } - threshold = thresholdHigh; - if (thresholdLow >= thresholdHigh) { - cellLimit = cellLimitLow; - if (cellLimitLow < cellLimitHigh && paintedHigh > paintedLow) { - cellLimit += ((cellLimitHigh - cellLimitLow) * - ((int)topCell - paintedLow) / - (paintedHigh - paintedLow)); - if (cellLimit > nextCellLimitHigh) - cellLimit = nextCellLimitHigh; - } - } else { - if (paintedHigh > paintedLow) { - threshold -= ((thresholdHigh - thresholdLow) * - ((int)topCell - paintedLow) / - (paintedHigh - paintedLow)); - if (threshold < nextThresholdLow) + + // Make new threshold value + threshold = thresholdHigh; + hasThresholdRange = thresholdLow < thresholdHigh; + if (hasThresholdRange && nCellsPaintedLow < nCellsPaintedHigh) { + // Linear interpolation + assert(nCellsPaintedLow <= nCellsToPaint); + threshold -= ((thresholdHigh - thresholdLow) * (nCellsToPaint - nCellsPaintedLow) / (nCellsPaintedHigh - nCellsPaintedLow)); + if (threshold < nextThresholdLow) { threshold = nextThresholdLow; + } } + assert(threshold <= thresholdHigh); + } else if (restart.nItemsPainted <= topCellItem && restart.valueSum < DBL_MAX) { + if (restart.nCellsPainted + rItemMinCells + adjLarge.nCells < nCellsToPaint) { + rItemHasExtraCell = true; + isLastTiebreak = true; + } + rItemIsDetermined = true; + } else { + assert(restart.nCellsPainted == nCellsToPaint); + break; } - assert(threshold <= thresholdHigh); } - // END OF THE PART THAT NEEDS REWRITING } static void GraphMeterMode_recordNewValue(Meter* this, const GraphDrawContext* context) { @@ -785,15 +870,14 @@ static void GraphMeterMode_recordNewValue(Meter* this, const GraphDrawContext* c scaleExp = 0; } // In IEEE 754 binary64 (DBL_MAX_EXP == 1024, DBL_MAX_10_EXP == 308), - // "scaleExp" never overflows. Assertion is written in a portable way. + // "scaleExp" never overflows. assert(DBL_MAX_10_EXP < 9864); assert(scaleExp <= INT16_MAX); valueStart[0].scaleExp = (int16_t)scaleExp; total = ldexp(1.0, scaleExp); } - if (total > DBL_MAX) { - total = DBL_MAX; // Cap to a finite value - } + if (total > DBL_MAX) + total = DBL_MAX; assert(graphHeight <= UINT16_MAX / 8); double maxDots = (double)(int)(graphHeight * 8); @@ -828,7 +912,7 @@ static void GraphMeterMode_recordNewValue(Meter* this, const GraphDrawContext* c numDots = 1; // Division of (sum / scaledTotal) underflows } - GraphMeterMode_computeColors(this, context, valueStart, deltaExp, scaledTotal, numDots); + GraphMeterMode_computeColors(this, context, valueStart, deltaExp, scaledTotal, (unsigned int)numDots); if (isPercentChart || !(scaledTotal < DBL_MAX) || (1U << deltaExp) >= graphHeight) { break; diff --git a/Meter.h b/Meter.h index d89ba0713..8650128e4 100644 --- a/Meter.h +++ b/Meter.h @@ -156,7 +156,9 @@ typedef struct GraphColorAdjOffset_ { } GraphColorAdjOffset; typedef struct GraphColorAdjStack_ { - double pointsSum; + double startPoint; + double fractionSum; + double valueSum; uint8_t nItems; } GraphColorAdjStack;