Skip to content

Native Gantt Chart / Horizontal Time Bar Support in MPAndroidChart #5516

@Dylan-lijl

Description

@Dylan-lijl

Is your feature request related to a problem? Please describe.
MPAndroidChart currently supports bar charts, line charts, pie charts, etc., but it lacks native support for Gantt charts or horizontal bars representing time intervals.

We often need charts where:

  • Y-axis represents tasks or steps
  • X-axis represents time
  • Each task/step is visualized as a horizontal rectangle spanning from start time to end time
  • Optionally, stacked segments, colors, labels, and values can be shown

Currently, achieving this requires writing a custom renderer, which is cumbersome and error-prone.

Describe the solution you'd like
Native support for a Gantt chart or horizontal time bar chart in MPAndroidChart that:

  • Supports horizontal bars representing start/end times
  • Handles labels, stacked segments, colors, and value display
  • Allows easy configuration without a custom renderer

Describe alternatives you've considered
We have implemented a custom renderer extending HorizontalBarChartRenderer to simulate this feature. Example:

public class SingleStackRender extends HorizontalBarChartRenderer {
    private RectF mBarShadowRectBuffer = new RectF();

    public SingleStackRender(BarDataProvider chart, ChartAnimator animator,
                             ViewPortHandler viewPortHandler) {
        super(chart, animator, viewPortHandler);
    }

    @Override
    public void drawDataSet(Canvas c, IBarDataSet dataSet, int index) {
        Transformer trans = mChart.getTransformer(dataSet.getAxisDependency());
        BarBuffer buffer = mBarBuffers[index];
        float phaseX = mAnimator.getPhaseX();
        float phaseY = mAnimator.getPhaseY();

        buffer.setPhases(phaseX, phaseY);
        buffer.setDataSet(index);
        buffer.setInverted(mChart.isInverted(dataSet.getAxisDependency()));
        buffer.setBarWidth(mChart.getBarData().getBarWidth());

        feedSingleStackBuffer(buffer, dataSet);
        trans.pointValuesToPixel(buffer.buffer);

        final boolean isSingleColor = dataSet.getColors().size() == 1;

        for (int j = 0; j < buffer.size(); j += 4) {
            if (!mViewPortHandler.isInBoundsTop(buffer.buffer[j + 3])) break;
            if (!mViewPortHandler.isInBoundsBottom(buffer.buffer[j + 1])) continue;

            mRenderPaint.setColor(isSingleColor ? dataSet.getColor() : dataSet.getColor(j / 4));
            c.drawRect(buffer.buffer[j], buffer.buffer[j + 1],
                       buffer.buffer[j + 2], buffer.buffer[j + 3], mRenderPaint);

            if (dataSet.getBarBorderWidth() > 0f) {
                mBarBorderPaint.setColor(dataSet.getBarBorderColor());
                mBarBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth()));
                c.drawRect(buffer.buffer[j], buffer.buffer[j + 1],
                           buffer.buffer[j + 2], buffer.buffer[j + 3], mBarBorderPaint);
            }
        }
    }

    private void feedSingleStackBuffer(BarBuffer buffer, IBarDataSet dataSet) {
        int bufferIndex = 0;
        float barWidth = mChart.getBarData().getBarWidth();
        float barWidthHalf = barWidth / 2f;

        for (int i = 0; i < dataSet.getEntryCount(); i++) {
            BarEntry entry = dataSet.getEntryForIndex(i);
            float[] vals = entry.getYVals();
            if (vals == null || vals.length < 2) continue;

            float start = vals[0];
            for (int k = 1; k < vals.length; k++) {
                float end = start + vals[k];
                buffer.buffer[bufferIndex++] = start;
                buffer.buffer[bufferIndex++] = entry.getX() - barWidthHalf;
                buffer.buffer[bufferIndex++] = end;
                buffer.buffer[bufferIndex++] = entry.getX() + barWidthHalf;
                start = end;
            }
        }
    }

    @Override
    public void drawValues(Canvas c) {
        if (!isDrawingValuesAllowed(mChart)) return;

        List<IBarDataSet> dataSets = mChart.getBarData().getDataSets();
        final float valueOffsetPlus = Utils.convertDpToPixel(5f);
        final boolean drawValueAboveBar = mChart.isDrawValueAboveBarEnabled();
        final float halfTextHeight = Utils.calcTextHeight(mValuePaint, "10") / 2f;

        for (int i = 0; i < mChart.getBarData().getDataSetCount(); i++) {
            IBarDataSet dataSet = dataSets.get(i);
            if (!shouldDrawValues(dataSet)) continue;

            applyValueTextStyle(dataSet);
            BarBuffer buffer = mBarBuffers[i];

            for (int j = 0; j < dataSet.getEntryCount(); j++) {
                BarEntry entry = dataSet.getEntryForIndex(j);
                float[] vals = entry.getYVals();
                if (vals == null || vals.length < 2) continue;

                int bufferIndex = j * 4 * (vals.length - 1) + (vals.length - 2) * 4;
                float left = buffer.buffer[bufferIndex];
                float top = buffer.buffer[bufferIndex + 1];
                float right = buffer.buffer[bufferIndex + 2];
                float bottom = buffer.buffer[bufferIndex + 3];
                float y = (top + bottom) / 2f;

                String formattedValue = dataSet.getValueFormatter().getBarLabel(entry);
                float valueTextWidth = Utils.calcTextWidth(mValuePaint, formattedValue);
                float xOffset = drawValueAboveBar ? valueOffsetPlus : -(valueTextWidth + valueOffsetPlus);
                if (mChart.isInverted(dataSet.getAxisDependency())) {
                    xOffset = -xOffset - valueTextWidth;
                }
                float x = right + xOffset;

                if (dataSet.isDrawValuesEnabled()) {
                    drawValue(c, formattedValue, x, y + halfTextHeight, dataSet.getValueTextColor(j));
                }
            }
        }
    }
}

Additional context
Example usage for creating a horizontal time bar chart (Gantt-like):

if (entity != null) {
    model.setRoot(entity.getRoot());
    for (ScriptPointEntity pointEntity : entity.getPoints()) {
        model.getPoints().add(pointEntity);
    }

    List<BarEntry> list = new ArrayList<>();
    List<Integer> colors = new ArrayList<>();
    int[] colorArray = getResources().getIntArray(R.array.script_info_chat_color);
    int colorLength = colorArray.length;

    for (int i = 0; i < entity.getActions().size(); i++) {
        ScriptActionEntity actionEntity = entity.getActions().get(i);
        colors.add(colorArray[actionEntity.getIndex() % colorLength]);
        list.add(new BarEntry(
            i,
            new float[]{actionEntity.getDownTime(), actionEntity.getUpTime() - actionEntity.getDownTime()},
            i // store index as data for labels
        ));
        model.getActions().add(actionEntity);
    }

    ThreadUtil.runOnUi(() -> {
        BarDataSet set = new BarDataSet(list, "步骤");
        AtomicInteger count = new AtomicInteger(2);

        set.setValueFormatter(new ValueFormatter() {
            @Override
            public String getBarStackedLabel(float a, BarEntry barEntry) {
                Integer index = (Integer) barEntry.getData();
                count.addAndGet(1);
                if (index == null || index >= model.getActions().size()) return "";
                ScriptActionEntity action = model.getActions().get(index);
                long d = action.getUpTime() - action.getDownTime();
                return count.get() % 2 == 0 ? d + "ms" : "";
            }

            @Override
            public String getBarLabel(BarEntry barEntry) {
                Integer index = (Integer) barEntry.getData();
                count.addAndGet(1);
                if (index == null || index >= model.getActions().size()) return "";
                ScriptActionEntity action = model.getActions().get(index);
                long d = action.getUpTime() - action.getDownTime();
                return d + "ms";
            }
        });

        set.setColors(colors);
        BarData data = new BarData(set);
        binding.flowChatLayout.horizontalBarChart.setData(data);
        binding.flowChatLayout.horizontalBarChart.invalidate();
    });
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions