From 0f0ec17ac09c2990fec51112dbbcd227cb58169a Mon Sep 17 00:00:00 2001 From: Bela Schaum Date: Wed, 13 Aug 2025 20:03:06 +0200 Subject: [PATCH 1/4] Add multiAxis --- src/base/geom/rect.h | 4 +- src/base/refl/auto_accessor.h | 2 +- src/base/type/uniquelist.h | 4 +- src/chart/generator/axis.cpp | 78 ++++++++++++++++++++++++----- src/chart/generator/axis.h | 48 ++++++++++++------ src/chart/generator/buckets.h | 2 +- src/chart/generator/plotbuilder.cpp | 70 +++++++++++++++++--------- src/chart/generator/plotbuilder.h | 4 +- src/chart/main/events.h | 29 +++++------ src/chart/main/style.cpp | 6 ++- src/chart/main/style.h | 3 ++ src/chart/main/stylesheet.cpp | 4 +- src/chart/options/autoparam.h | 1 + src/chart/options/channel.h | 59 +++++++++++++++++++++- src/chart/options/options.cpp | 10 ++-- src/chart/options/options.h | 71 ++++++++++++++++++++------ src/chart/rendering/drawaxes.cpp | 22 ++++---- src/chart/rendering/drawaxes.h | 2 +- src/chart/rendering/drawlegend.cpp | 14 +++--- src/chart/rendering/drawlegend.h | 2 +- src/dataframe/impl/data_source.cpp | 2 +- src/dataframe/old/datatable.cpp | 12 +++-- src/dataframe/old/datatable.h | 2 +- src/dataframe/old/types.h | 2 +- 24 files changed, 328 insertions(+), 125 deletions(-) diff --git a/src/base/geom/rect.h b/src/base/geom/rect.h index 343fa5a90..77451b3b1 100644 --- a/src/base/geom/rect.h +++ b/src/base/geom/rect.h @@ -170,10 +170,10 @@ template Rect Rect::Boundary(const Container &points) { auto &&[minx, maxx] = std::ranges::minmax( - std::ranges::views::transform(points, std::mem_fn(&Point::x)), + std::views::transform(points, std::mem_fn(&Point::x)), Math::Floating::less); auto &&[miny, maxy] = std::ranges::minmax( - std::ranges::views::transform(points, std::mem_fn(&Point::y)), + std::views::transform(points, std::mem_fn(&Point::y)), Math::Floating::less); return Rect{{minx, miny}, {maxx - minx, maxy - miny}}; } diff --git a/src/base/refl/auto_accessor.h b/src/base/refl/auto_accessor.h index 9e90a9f89..3b0366215 100644 --- a/src/base/refl/auto_accessor.h +++ b/src/base/refl/auto_accessor.h @@ -153,7 +153,7 @@ const Accessor &getAccessor( template auto getAccessorNames() { - return std::ranges::views::keys(getAccessors()); + return std::views::keys(getAccessors()); } } diff --git a/src/base/type/uniquelist.h b/src/base/type/uniquelist.h index 767aa4e49..d9ae1e8ce 100644 --- a/src/base/type/uniquelist.h +++ b/src/base/type/uniquelist.h @@ -2,6 +2,8 @@ #define TYPE_UNIQUELIST #include +#include +#include #include #include #include @@ -217,7 +219,7 @@ template class UniqueList [[nodiscard]] auto as_set() const noexcept { - return std::ranges::views::keys(items); + return std::views::keys(items); } template diff --git a/src/chart/generator/axis.cpp b/src/chart/generator/axis.cpp index 40ec4be77..6768fb57f 100644 --- a/src/chart/generator/axis.cpp +++ b/src/chart/generator/axis.cpp @@ -1,7 +1,10 @@ #include "axis.h" +#include <__compare/compare_three_way.h> #include #include +#include +#include #include #include #include @@ -46,7 +49,10 @@ void Axises::addLegendInterpolation(double legendFactor, && target.measure.enabled.get()) || (!source.dimension.empty() && !target.dimension.empty())) - && source.seriesName() != target.seriesName()) { + && source.seriesName() != target.seriesName() + && !DimensionAxis::commonDimensionParts( + source.dimension.getValues(), + target.dimension.getValues())) { if (!leftLegend[0]) leftLegend[0].emplace(legendType); if (!leftLegend[1]) leftLegend[1].emplace(legendType); @@ -67,8 +73,11 @@ void Axises::addLegendInterpolation(double legendFactor, leftLegend[0] && leftLegend[1] && leftLegend[0]->interpolated == leftLegend[1]->interpolated - && leftLegend[0]->calc.seriesName() - == leftLegend[1]->calc.seriesName(); + && (leftLegend[0]->calc.seriesName() + == leftLegend[1]->calc.seriesName() + || DimensionAxis::commonDimensionParts( + leftLegend[0]->calc.dimension.getValues(), + leftLegend[1]->calc.dimension.getValues())); sameInterpolated && !leftLegend[0]->calc.dimension.empty() && !leftLegend[1]->calc.dimension.empty()) { @@ -100,6 +109,19 @@ Geom::Point Axises::origo() const return {at(AxisId::x).measure.origo(), at(AxisId::y).measure.origo()}; } +std::size_t DimensionAxis::commonDimensionParts(const Values &lhs, + const Values &rhs) +{ + return lhs.empty() || rhs.empty() + ? 0 + : std::ranges::mismatch(lhs.begin()->first, + rhs.begin()->first, + [](const auto &lhsItem, const auto &rhsItem) + { + return lhsItem.column == rhsItem.column; + }).in1 + - lhs.begin()->first.begin(); +} MeasureAxis::MeasureAxis(const Math::Range<> &interval, std::string &&series, @@ -242,7 +264,7 @@ MeasureAxis interpolate(const MeasureAxis &op0, return res; } -bool DimensionAxis::add(const Data::SliceIndex &index, +bool DimensionAxis::add(const std::vector &index, const Math::Range<> &range, std::uint32_t position, const std::optional &color, @@ -302,27 +324,57 @@ DimensionAxis interpolate(const DimensionAxis &op0, factor)); using Val = DimensionAxis::Values::value_type; + std::size_t commonSize = + DimensionAxis::commonDimensionParts(op0.getValues(), + op1.getValues()); + std::size_t maxSize = std::max( + op0.values.empty() ? 0 : op0.values.begin()->first.size(), + op1.values.empty() ? 0 : op1.values.begin()->first.size()); + const Val *latest1{}; const Val *latest2{}; - auto merger = [&](const Val &lhs, const Val &rhs) -> Val + auto merger = [&](const Val &lhs, + const Val &rhs, + const Val::first_type *key = nullptr) -> Val + { + printf("%s to %s\n", + DimensionAxis::mergedLabels(lhs.first).c_str(), + DimensionAxis::mergedLabels(rhs.first).c_str()); + return {key ? *key : lhs.first, + interpolate(lhs.second, rhs.second, factor)}; + }; + + auto needMerge = [&](const Val &lhs, const Val &rhs) { latest1 = std::addressof(lhs); latest2 = std::addressof(rhs); - return {lhs.first, - interpolate(lhs.second, rhs.second, factor)}; + return commonSize == maxSize || commonSize == 0; + }; + + auto comparator = [&](const auto &lhs, const auto &rhs) + { + if (commonSize == maxSize || commonSize == 0) + return std::compare_three_way{}(lhs, rhs); + return std::lexicographical_compare_three_way(lhs.begin(), + lhs.begin() + commonSize, + rhs.begin(), + rhs.begin() + commonSize); }; auto &&one_side = - [&merger](bool first, + [&](bool first, DimensionAxis::Item::PosType DimensionAxis::Item::*pos, const Val *¶mOther) { return [&, first, pos](const Val &val) -> Val { - if (paramOther && paramOther->first == val.first) { - auto &&res = first ? merger(val, *paramOther) - : merger(*paramOther, val); + if (paramOther + && std::is_eq( + comparator(paramOther->first, val.first))) { + auto &&res = first + ? merger(val, *paramOther) + : merger(*paramOther, val, &val.first); (res.second.*pos).makeAuto(); return res; } @@ -334,11 +386,13 @@ DimensionAxis interpolate(const DimensionAxis &op0, op1.values, res.values, Alg::merge_args{.projection = &Val::first, + .comparator = comparator, .transformer_1 = one_side(true, &DimensionAxis::Item::endPos, latest2), .transformer_2 = one_side(false, &DimensionAxis::Item::startPos, latest1), + .need_merge = needMerge, .merger = merger}); return res; @@ -438,7 +492,7 @@ interpolate(const SplitAxis &op0, const SplitAxis &op1, double factor) } else res.parts - .insert({std::nullopt, + .insert({{}, {.weight = op0.parts.empty() ? 1 - factor : factor}}) ->second.unique = true; diff --git a/src/chart/generator/axis.h b/src/chart/generator/axis.h index b3b5d171f..fd331e895 100644 --- a/src/chart/generator/axis.h +++ b/src/chart/generator/axis.h @@ -2,6 +2,7 @@ #define AXIS_H #include +#include #include "base/anim/interpolated.h" #include "base/geom/point.h" @@ -21,7 +22,7 @@ namespace Vizzu::Gen struct ChannelStats { using TrackType = std::variant, - std::map>; + std::map>>; Refl::EnumArray tracked; Math::Range<> lightness; @@ -35,9 +36,9 @@ struct ChannelStats void track(ChannelId at, const Data::MarkerId &id) { auto &vec = std::get<1>(tracked[at]); - if (id.label) + if (!id.label.empty()) vec.try_emplace(static_cast(id.itemId), - *id.label); + id.label); } void track(ChannelId at, const double &value) @@ -128,13 +129,13 @@ struct DimensionAxis friend Item interpolate(const Item &op0, const Item &op1, double factor); }; - using Values = std::multimap; + using Values = std::multimap, Item>; double factor{}; bool hasMarker{}; DimensionAxis() = default; - bool add(const Data::SliceIndex &index, + bool add(const std::vector &index, const Math::Range<> &range, std::uint32_t position, const std::optional &color, @@ -145,19 +146,19 @@ struct DimensionAxis [[nodiscard]] auto begin() { - return std::ranges::views::values(values).begin(); + return std::views::values(values).begin(); }; [[nodiscard]] auto end() { - return std::ranges::views::values(values).end(); + return std::views::values(values).end(); } [[nodiscard]] auto begin() const { - return std::ranges::views::values(values).begin(); + return std::views::values(values).begin(); }; [[nodiscard]] auto end() const { - return std::ranges::views::values(values).end(); + return std::views::values(values).end(); } [[nodiscard]] bool empty() const { return values.empty(); } bool setLabels(double step); @@ -179,6 +180,23 @@ struct DimensionAxis ItemSorterByRangeStart>{begin(), end()}; } + static std::size_t commonDimensionParts(const Values &lhs, + const Values &rhs); + + template + [[nodiscard]] static std::string mergedLabels( + const std::vector &slices) + { + return {std::from_range, + std::views::transform(slices, + [](const Data::SliceIndex &slice) + { + return ", " + slice.*which; + }) + | std::views::join | std::views::drop(2)}; + } + private: Values values; }; @@ -189,10 +207,13 @@ struct Axis MeasureAxis measure; DimensionAxis dimension; - [[nodiscard]] const std::string &seriesName() const + [[nodiscard]] std::string seriesName() const { - if (!dimension.empty()) - return dimension.getValues().begin()->first.column; + if (!dimension.empty()) { + return DimensionAxis::mergedLabels< + &Data::SliceIndex::column>( + dimension.getValues().begin()->first); + } return measure.series; } @@ -220,8 +241,7 @@ struct SplitAxis : Axis } }; - using Parts = - std::multimap, Part>; + using Parts = std::multimap, Part>; Parts parts; [[nodiscard]] bool operator==( diff --git a/src/chart/generator/buckets.h b/src/chart/generator/buckets.h index f63b21fb5..a73338c37 100644 --- a/src/chart/generator/buckets.h +++ b/src/chart/generator/buckets.h @@ -36,7 +36,7 @@ struct Buckets [[nodiscard]] auto operator*() const { - return std::ranges::views::transform(data, + return std::views::transform(data, [this](Marker *marker) -> std::pair { diff --git a/src/chart/generator/plotbuilder.cpp b/src/chart/generator/plotbuilder.cpp index abd4a7727..3c71a4447 100644 --- a/src/chart/generator/plotbuilder.cpp +++ b/src/chart/generator/plotbuilder.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include "dataframe/old/datatable.h" #include "dataframe/old/types.h" +#include "axis.h" #include "buckets.h" #include "colorbase.h" #include "plot.h" @@ -157,10 +159,12 @@ std::vector PlotBuilder::sortedBuckets( it = sorted.emplace(it, idx.itemId, 0.0, - (marker.*buckets.marker_id_get).label - ? &(marker.*buckets.marker_id_get) - .label->value - : nullptr); + !(marker.*buckets.marker_id_get).label.empty() + ? std::make_optional( + DimensionAxis::mergedLabels( + (marker.*buckets.marker_id_get) + .label)) + : std::nullopt); it->size += marker.size.getCoord( !plot->getOptions()->getOrientation()); @@ -181,8 +185,9 @@ std::vector PlotBuilder::sortedBuckets( std::ranges::stable_sort(sorted, [](const BucketSortInfo &lhs, const BucketSortInfo &rhs) { - if (rhs.label == nullptr || lhs.label == nullptr) - return lhs.label != nullptr; + if (rhs.label == std::nullopt + || lhs.label == std::nullopt) + return lhs.label != std::nullopt; return Text::NaturalCmp{}(*lhs.label, *rhs.label); }); break; @@ -271,7 +276,7 @@ bool PlotBuilder::linkMarkers(const Buckets &buckets, for (std::size_t ix{}, max = sorted.size(); ix < max; ++ix) { auto &o = dimOffset[ix]; for (const auto &bucket : buckets) { - auto &&ids = std::ranges::views::values(bucket); + auto &&ids = std::views::values(bucket); auto sIx = sorted[ix].index; auto it = std::ranges::lower_bound(ids, sIx, @@ -308,7 +313,7 @@ bool PlotBuilder::linkMarkers(const Buckets &buckets, std::array prevPositions{}; for (auto i = 0U; i < sorted.size(); ++i) { auto idAct = sorted[i].index; - auto &&ids = std::ranges::views::values(bucket); + auto &&ids = std::views::values(bucket); auto it = std::ranges::lower_bound(ids, idAct, {}, @@ -429,10 +434,10 @@ void PlotBuilder::calcLegendAndLabel(const Data::DataTable &dataTable) } } else if (!scale.isEmpty()) { - auto merge = - type == LegendId::size - || (type == LegendId::lightness - && plot->getOptions()->dimLabelIndex(+type) == 0); + auto merge = type == LegendId::size + || (type == LegendId::lightness + && plot->getOptions()->dimLabelIndex(+type) + == std::vector{std::size_t{}}); for (std::uint32_t count{}; const auto &[i, label] : std::get<1>(stats.at(type))) { auto rangeId = static_cast(i); @@ -455,8 +460,16 @@ void PlotBuilder::calcLegendAndLabel(const Data::DataTable &dataTable) } if (auto &&series = plot->getOptions()->labelSeries(type); - series && isAutoTitle && calcLegend.dimension.empty()) - calcLegend.title = series.value().getColIndex(); + !series.empty() && isAutoTitle + && calcLegend.dimension.empty()) + calcLegend.title = std::string{std::from_range, + series + | std::views::transform( + [](const Data::SeriesIndex &series) + { + return ", " + series.getColIndex(); + }) + | std::views::join | std::views::drop(2)}; } } @@ -498,7 +511,7 @@ void PlotBuilder::calcLegendAndLabel(const Data::DataTable &dataTable) markerLabelsUnitPercent ? "%" : dataTable.get_series_info(meas->getColIndex(), - "unit")}}, + "unit")}}, ::Anim::String{meas->getColIndex()}}; } } @@ -532,11 +545,11 @@ void PlotBuilder::calcAxis(const Data::DataTable &dataTable, axisProps.step.getValue()}; } else { - for (auto merge = - axisProps.sort == Sort::byLabel - || (plot->getOptions()->dimLabelIndex(+type) == 0 - && (axisProps.sort == Sort::none - || scale.dimensions().size() == 1)); + for (auto merge = axisProps.sort == Sort::byLabel + || (plot->getOptions()->dimLabelIndex(+type) + == std::vector{std::size_t{}} + && (axisProps.sort == Sort::none + || scale.dimensions().size() == 1)); const auto &marker : plot->markers) { if (!marker.enabled) continue; @@ -545,8 +558,8 @@ void PlotBuilder::calcAxis(const Data::DataTable &dataTable, ? marker.mainId : marker.subId; - if (const auto &slice = id.label) - axis.dimension.add(*slice, + if (const auto &slice = id.label; !slice.empty()) + axis.dimension.add(slice, marker.getSizeBy(type), {}, {}, @@ -557,8 +570,15 @@ void PlotBuilder::calcAxis(const Data::DataTable &dataTable, } if (auto &&series = plot->getOptions()->labelSeries(type); !axis.dimension.setLabels(axisProps.step.getValue(1.0)) - && series && isAutoTitle) - axis.title = series.value().getColIndex(); + && !series.empty() && isAutoTitle) + axis.title = std::string{std::from_range, + series + | std::views::transform( + [](const Data::SeriesIndex &series) + { + return ", " + series.getColIndex(); + }) + | std::views::join | std::views::drop(2)}; for (std::uint32_t pos{}; DimensionAxis::Item & i : axis.dimension.sortedItems()) i.endPos = i.startPos = @@ -641,7 +661,7 @@ PlotBuilder::addSeparation(const Buckets &buckets, AxisId axisIndex) } auto &resItem = *it; - if (!resItem.index && idx.label) + if (resItem.index.empty() && !idx.label.empty()) resItem.index = idx.label; resItem.containsValues.include( diff --git a/src/chart/generator/plotbuilder.h b/src/chart/generator/plotbuilder.h index 8616053a4..cd5fdf951 100644 --- a/src/chart/generator/plotbuilder.h +++ b/src/chart/generator/plotbuilder.h @@ -31,13 +31,13 @@ class PlotBuilder { std::size_t index{}; double size{}; - const std::string *label{}; + std::optional label{}; }; struct BucketSeparationInfo { std::size_t itemId{}; - std::optional index{}; + std::vector index{}; Math::Range<> containsValues{0.0, 0.0}; Math::Range<> atRange{0.0, 0.0}; diff --git a/src/chart/main/events.h b/src/chart/main/events.h index db8a3ad72..43ace408b 100644 --- a/src/chart/main/events.h +++ b/src/chart/main/events.h @@ -78,7 +78,7 @@ class Events Geom::TransformedRect outerRect; [[maybe_unused]] Geom::Rect innerRect; [[maybe_unused]] double align{}; - std::string_view text; + std::string text; }; struct OnTextDrawEvent : @@ -294,14 +294,13 @@ class Events template explicit CategoryInfo( - const std::string_view &categoryName, - const std::string_view &categoryValue, + const std::vector &indices, Args &&...args) : Base(std::forward(args)...) { - Conv::JSONObj{info}.template operator()( - categoryName, - categoryValue); + Conv::JSONObj cats{info}; + for (auto &&[name, value] : indices) + cats.operator()(name, value); } void appendToJSON(Conv::JSONObj &&jsonObj) const override @@ -384,13 +383,12 @@ class Events } static auto dimLegendLabel( - const std::string_view &categoryName, + const std::vector &indices, const std::string &categoryValue, const LegendProperties &properties) { return std::make_unique>>( - categoryName, - categoryValue, + indices, categoryValue, "label", properties); @@ -412,13 +410,12 @@ class Events properties); } - static auto legendMarker(const std::string_view &categoryName, - const std::string_view &categoryValue, + static auto legendMarker( + const std::vector &indices, const LegendProperties &properties) { return std::make_unique>( - categoryName, - categoryValue, + indices, "marker", properties); } @@ -428,13 +425,13 @@ class Events return std::make_unique("bar", properties); } - static auto dimAxisLabel(const std::string_view &categoryName, + static auto dimAxisLabel( + const std::vector &indices, const std::string &categoryValue, Gen::AxisId axis) { return std::make_unique>>( - categoryName, - categoryValue, + indices, categoryValue, "label", axis); diff --git a/src/chart/main/style.cpp b/src/chart/main/style.cpp index af55752cb..1e505cfa4 100644 --- a/src/chart/main/style.cpp +++ b/src/chart/main/style.cpp @@ -265,7 +265,8 @@ Chart Chart::def() AxisLabelParams { .position = Anim::Interpolated(AxisLabel::Position::min_edge), - .side = Anim::Interpolated(AxisLabel::Side::negative) + .side = Anim::Interpolated(AxisLabel::Side::negative), + .multiAxis = Anim::Interpolated(AxisLabel::MultiAxis::split) } }, .ticks = { @@ -357,7 +358,8 @@ Chart Chart::def() AxisLabelParams { .position = Anim::Interpolated(AxisLabel::Position::min_edge), - .side = Anim::Interpolated(AxisLabel::Side::negative) + .side = Anim::Interpolated(AxisLabel::Side::negative), + .multiAxis = Anim::Interpolated(AxisLabel::MultiAxis::split) } }, .ticks = { diff --git a/src/chart/main/style.h b/src/chart/main/style.h index 779a4770b..e72be1aa1 100644 --- a/src/chart/main/style.h +++ b/src/chart/main/style.h @@ -201,8 +201,11 @@ struct AxisLabelParams enum class Side : std::uint8_t { positive, negative }; + enum class MultiAxis : std::uint8_t { split, separate }; + Param<::Anim::Interpolated> position; Param<::Anim::Interpolated> side; + Param<::Anim::Interpolated> multiAxis; }; struct AxisLabel : OrientedLabel, AxisLabelParams diff --git a/src/chart/main/stylesheet.cpp b/src/chart/main/stylesheet.cpp index 7f16932e6..aee3c552f 100644 --- a/src/chart/main/stylesheet.cpp +++ b/src/chart/main/stylesheet.cpp @@ -243,8 +243,8 @@ void Sheet::setAfterStyles(Gen::Plot &plot, const Geom::Size &size) if (!item.label.get()) continue; - auto textBoundary = - Gfx::ICanvas::textBoundary(font, label.value); + auto textBoundary = Gfx::ICanvas::textBoundary(font, + Gen::DimensionAxis::mergedLabels(label)); auto textXHalfMargin = xLabel.toInvMargin(textBoundary, font.size) .getSpace() diff --git a/src/chart/options/autoparam.h b/src/chart/options/autoparam.h index 832e77636..3cbfe2cb7 100644 --- a/src/chart/options/autoparam.h +++ b/src/chart/options/autoparam.h @@ -37,6 +37,7 @@ template struct AutoParam } [[nodiscard]] auto toString() const + requires(requires { Conv::toString(std::declval()); }) { if (autoSet) return decltype(Conv::toString(value)){"auto"}; if constexpr (nullable) diff --git a/src/chart/options/channel.h b/src/chart/options/channel.h index e0154ec1c..82b60df39 100644 --- a/src/chart/options/channel.h +++ b/src/chart/options/channel.h @@ -1,11 +1,15 @@ #ifndef CHANNEL_H #define CHANNEL_H +#include +#include #include #include +#include #include #include +#include "base/conv/auto_json.h" #include "base/geom/orientation.h" #include "dataframe/old/types.h" @@ -181,10 +185,59 @@ struct Channel return set.measureId; } + struct LabelLevelList + { + std::vector levels; + + [[nodiscard]] bool operator==( + const LabelLevelList &other) const = default; + + [[nodiscard]] auto operator<=>( + const LabelLevelList &) const = default; + + LabelLevelList(std::size_t one) : levels{one} {} + template LabelLevelList(T &&) = delete; + + explicit LabelLevelList(std::vector &&levels) : + levels(std::move(levels)) + {} + + static LabelLevelList fromString(const std::string &str) + { + return LabelLevelList{{std::from_range, + std::string_view{str} | std::views::split(',') + | std::views::transform( + [](const auto &s) + { + auto &&range = + s + | std::views::drop_while( + [](const char &c) + { + return std::isspace(c) + || c == '['; + }); + std::size_t ix{}; + std::from_chars(range.begin(), + range.end(), + ix); + return ix; + })}}; + } + + [[nodiscard]] std::string toString() const + { + if (levels.size() == 1) + return std::to_string(levels.front()); + + return Conv::toJSON(levels); + } + }; + bool stackable{}; ChannelSeriesList set{}; ChannelRange range{}; - Base::AutoParam labelLevel{}; + Base::AutoParam labelLevel{}; Base::AutoParam title{}; }; @@ -200,7 +253,9 @@ struct Channel template concept ChannelIdLike = requires(const T &v) { - { +v } -> std::same_as; + { + +v + } -> std::same_as; }; template diff --git a/src/chart/options/options.cpp b/src/chart/options/options.cpp index 1d7bd5998..f720b5cb7 100644 --- a/src/chart/options/options.cpp +++ b/src/chart/options/options.cpp @@ -291,7 +291,7 @@ std::optional Options::getAutoLegend() const series.erase(*meas); for (auto axisId : {AxisId::x, AxisId::y}) - if (auto &&id = labelSeries(axisId)) series.erase(*id); + for (auto &&id : labelSeries(axisId)) series.erase(id); for (auto channelId : {LegendId::color, LegendId::lightness}) if (channels.at(channelId).dimensions().contains_any( @@ -350,9 +350,11 @@ void Options::setRange(Channel &channel, bool Options::labelsShownFor(const Data::SeriesIndex &series) const { - return labelSeries(AxisId::x) == series - || labelSeries(AxisId::y) == series - || (legend.get() && labelSeries(*legend.get()) == series); + return std::ranges::contains(labelSeries(AxisId::x), series) + || std::ranges::contains(labelSeries(AxisId::y), series) + || (legend.get() + && std::ranges::contains(labelSeries(*legend.get()), + series)); } void Options::showTooltip(std::optional marker) diff --git a/src/chart/options/options.h b/src/chart/options/options.h index f300f7b9c..60279a8f3 100644 --- a/src/chart/options/options.h +++ b/src/chart/options/options.h @@ -1,11 +1,14 @@ #ifndef PLOT_OPTIONS_H #define PLOT_OPTIONS_H +#include #include #include #include #include +#include #include +#include #include "base/anim/interpolated.h" #include "base/geom/orientation.h" @@ -89,37 +92,77 @@ class Options : public OptionProperties return channels.at(subAxisType()); } - [[nodiscard]] std::optional dimLabelIndex( + [[nodiscard]] std::vector dimLabelIndex( ChannelId channel) const { auto &&ch = channels.at(channel); auto hasMeasure = ch.hasMeasure(); - auto defaultLabelLevel = + + auto defaultLabelDimension = hasMeasure && channel == mainAxisType() && ch.hasDimension() && geometry == ShapeType::rectangle; - auto ll = ch.labelLevel.getValue(defaultLabelLevel); - if (hasMeasure && ll == 0) return {}; - ll -= hasMeasure; - if (ll >= ch.dimensions().size()) return {}; - return ll; + auto lll = ch.labelLevel.getValueOrAuto(); + if (!lll && defaultLabelDimension) { + lll.emplace(std::size_t{1}); + } + else if (!lll && !hasMeasure && ch.dimensions().size() > 1 + && channel == legend.get().getValueOrAuto()) { + lll.emplace( + Channel::LabelLevelList{std::vector{std::from_range, + std::views::iota(std::size_t{}, + ch.dimensions().size())}}); + } + else if (!lll) { + lll.emplace(std::size_t{0}); + } + + if (hasMeasure && std::ranges::contains(lll->levels, 0)) + return {}; + if (hasMeasure) + std::ranges::transform(lll->levels, + lll->levels.begin(), + [](auto &&v) + { + return v - 1; + }); + std::erase_if(lll->levels, + [max = ch.dimensions().size()](auto &v) + { + return v >= max; + }); + return lll->levels; } [[nodiscard]] bool isMeasure(ChannelId channel) const { return channels.at(channel).hasMeasure() - && !dimLabelIndex(channel); + && dimLabelIndex(channel).empty(); } template - [[nodiscard]] Channel::OptionalIndex labelSeries( + [[nodiscard]] std::vector labelSeries( IdType channel) const { auto &&ch = channels.at(channel); - if (auto dimIndex = dimLabelIndex(+channel)) - return *std::next(ch.set.dimensionIds.begin(), - static_cast(*dimIndex)); - return ch.labelLevel.getValue(0) == 0 ? ch.measure() - : std::nullopt; + if (auto dimIndex = dimLabelIndex(+channel); + !dimIndex.empty()) { + std::vector result(dimIndex.size()); + std::transform(dimIndex.begin(), + dimIndex.end(), + result.begin(), + [&ch](std::size_t index) + { + return *std::next(ch.set.dimensionIds.begin(), + static_cast(index)); + }); + return result; + } + if (auto &&measure = ch.measure(); + measure + && ch.labelLevel.getValue(std::size_t{}).levels + == std::vector{std::size_t{}}) + return {*measure}; + return {}; } Channel &mainAxis() { return channels.at(mainAxisType()); } diff --git a/src/chart/rendering/drawaxes.cpp b/src/chart/rendering/drawaxes.cpp index 6efa9ff3d..6b59e793f 100644 --- a/src/chart/rendering/drawaxes.cpp +++ b/src/chart/rendering/drawaxes.cpp @@ -138,7 +138,7 @@ const DrawAxes &&DrawAxes::init() && const auto &axis = plot->axises.at(axisIndex); const static Gen::SplitAxis::Parts oneSized{ - {std::nullopt, Gen::SplitAxis::Part{}}}; + {{}, Gen::SplitAxis::Part{}}}; splits[axisIndex] = axis.parts.empty() ? oneSized : axis.parts; @@ -467,16 +467,15 @@ void DrawAxes::drawTitle(Gen::AxisId axisIndex, -std::numbers::pi / 2.0 * (fades == ::Anim::second ? !isHorizontal( - titleStyle.orientation->get_or_first(index) - .value) + titleStyle.orientation->get_or_first(index) + .value) : titleStyle.orientation->factor( - Styles::AxisTitle::Orientation::vertical)); + Styles::AxisTitle::Orientation::vertical)); auto orientedSize = fades == ::Anim::second ? calcOrientation( - titleStyle.orientation->get_or_first(index) - .value) + titleStyle.orientation->get_or_first(index).value) : titleStyle.orientation->combine(calcOrientation); auto center = offset * (orientedSize / 2.0); @@ -579,7 +578,7 @@ void DrawAxes::drawDimensionLabel(Gen::AxisId axisIndex, ? labelStyle.side->get_or_first(index).value == Styles::AxisLabel::Side::negative : labelStyle.side->factor( - Styles::AxisLabel::Side::negative); + Styles::AxisLabel::Side::negative); auto draw = [&, posDir = coordSys @@ -591,8 +590,10 @@ void DrawAxes::drawDimensionLabel(Gen::AxisId axisIndex, double plusWeight = 1.0) { if (!str.value) return; + auto dimValues = + Gen::DimensionAxis::mergedLabels(dimInfo.index); drawLabel.draw(canvas, - dimInfo.index.value, + dimValues, posDir, labelStyle, 0, @@ -601,9 +602,8 @@ void DrawAxes::drawDimensionLabel(Gen::AxisId axisIndex, str.weight, plusWeight)), *rootEvents.draw.plot.axis.label, - Events::Targets::dimAxisLabel( - dimInfo.index.column, - dimInfo.index.value, + Events::Targets::dimAxisLabel(dimInfo.index, + dimValues, axisIndex)); }; diff --git a/src/chart/rendering/drawaxes.h b/src/chart/rendering/drawaxes.h index 15ced15c8..17f875194 100644 --- a/src/chart/rendering/drawaxes.h +++ b/src/chart/rendering/drawaxes.h @@ -42,7 +42,7 @@ class DrawAxes : public DrawingContext { struct DimLabel { - const Data::SliceIndex &index; + const std::vector &index; const ::Anim::Interpolated &presented; bool start; bool end; diff --git a/src/chart/rendering/drawlegend.cpp b/src/chart/rendering/drawlegend.cpp index 9b2729d2f..0fd1983d4 100644 --- a/src/chart/rendering/drawlegend.cpp +++ b/src/chart/rendering/drawlegend.cpp @@ -189,13 +189,15 @@ void DrawLegend::drawDimension(Info &info) const getMarkerRect(info, itemRect), needGradient); + auto &&value = Gen::DimensionAxis::mergedLabels(sindex); + label.draw(info.canvas, getLabelRect(info, itemRect), - sindex.value, + value, style.label, *events.label, - Events::Targets::dimLegendLabel(sindex.column, - sindex.value, + Events::Targets::dimLegendLabel(sindex, + value, info.properties), {.colorTransform = Gfx::ColorTransform::Opacity(alpha), .gradient = @@ -235,7 +237,7 @@ Geom::TransformedRect DrawLegend::getLabelRect(const Info &info, } void DrawLegend::drawMarker(Info &info, - const Data::SliceIndex &sindex, + const std::vector &sindex, const Gfx::Color &color, const Geom::Rect &rect, bool needGradient) const @@ -251,9 +253,7 @@ void DrawLegend::drawMarker(Info &info, info.canvas.setLineWidth(0); if (auto &&markerElement{ - Events::Targets::legendMarker(sindex.column, - sindex.value, - info.properties)}; + Events::Targets::legendMarker(sindex, info.properties)}; events.marker->invoke( Events::OnRectDrawEvent(*markerElement, {rect, false}))) { auto radius = rootStyle.legend.marker.type->factor( diff --git a/src/chart/rendering/drawlegend.h b/src/chart/rendering/drawlegend.h index 3c0987c88..a7d610289 100644 --- a/src/chart/rendering/drawlegend.h +++ b/src/chart/rendering/drawlegend.h @@ -59,7 +59,7 @@ class DrawLegend : public DrawingContext void drawMeasure(const Info &info) const; void drawMarker(Info &info, - const Data::SliceIndex &sindex, + const std::vector &sindex, const Gfx::Color &color, const Geom::Rect &rect, bool needGradient) const; diff --git a/src/dataframe/impl/data_source.cpp b/src/dataframe/impl/data_source.cpp index d006265f1..fff377518 100644 --- a/src/dataframe/impl/data_source.cpp +++ b/src/dataframe/impl/data_source.cpp @@ -65,7 +65,7 @@ constexpr void index_erase_if<>::operator()( Cont &&cont) const noexcept { auto from = cont.end(); - for (auto i : std::ranges::views::reverse(indices)) + for (auto i : std::views::reverse(indices)) cont[i] = std::move(*--from); std::forward(cont).erase(from, cont.end()); } diff --git a/src/dataframe/old/datatable.cpp b/src/dataframe/old/datatable.cpp index 89d06dafc..b6f14b22d 100644 --- a/src/dataframe/old/datatable.cpp +++ b/src/dataframe/old/datatable.cpp @@ -198,20 +198,24 @@ const std::string &DataCube::getName( } MarkerId DataCube::getId(const SeriesList &sl, - const std::optional &ll, + const std::vector &ll, const MultiIndex &mi) const { MarkerId res{}; + res.label.resize(ll.size()); std::vector> v(sl.size()); for (auto &&[val, comm] : dim_reindex.iterate_common(sl)) { auto &&[name, cats, size, ix] = val; auto &&oldIx = mi.old[ix]; if (comm) { - if (v[*comm] = {oldIx, size}; *comm == ll) - res.label.emplace(name, + v[*comm] = {oldIx, size}; + if (auto &&it = std::ranges::find(ll, *comm); + it != ll.end()) { + res.label[it - ll.begin()] = SliceIndex{name, oldIx < cats.size() ? cats[oldIx] - : std::string{}); + : std::string{}}; + } } else res.seriesId = res.seriesId * size + oldIx; diff --git a/src/dataframe/old/datatable.h b/src/dataframe/old/datatable.h index c8370469b..a416fdd49 100644 --- a/src/dataframe/old/datatable.h +++ b/src/dataframe/old/datatable.h @@ -74,7 +74,7 @@ class DataCube const SeriesIndex &seriesId) const; [[nodiscard]] MarkerId getId(const SeriesList &, - const std::optional &, + const std::vector &, const MultiIndex &) const; [[nodiscard]] std::string joinDimensionValues( diff --git a/src/dataframe/old/types.h b/src/dataframe/old/types.h index 430044daf..6efec4f3b 100644 --- a/src/dataframe/old/types.h +++ b/src/dataframe/old/types.h @@ -146,7 +146,7 @@ struct MultiIndex struct MarkerId { - std::optional label; + std::vector label; std::size_t seriesId{}; std::size_t itemId{}; From fe9e9c0ee6b036674ce8a13f3f69438943c85290 Mon Sep 17 00:00:00 2001 From: Bela Schaum Date: Sat, 20 Sep 2025 08:56:11 +0200 Subject: [PATCH 2/4] clang-tidy + fix tests --- src/base/math/renard.h | 2 +- src/chart/generator/axis.cpp | 6 +----- src/chart/generator/axis.h | 4 ++-- src/chart/generator/plotbuilder.cpp | 28 ++++++++++++++-------------- src/chart/options/channel.h | 21 ++++++++++++++++----- src/chart/options/options.cpp | 1 + src/chart/options/options.h | 10 ++++++---- src/chart/rendering/drawaxes.cpp | 1 - src/chart/rendering/drawlegend.cpp | 1 + test/e2e/test_cases/test_cases.json | 6 +++--- 10 files changed, 45 insertions(+), 35 deletions(-) diff --git a/src/base/math/renard.h b/src/base/math/renard.h index 481deb1c2..1ec8872e7 100644 --- a/src/base/math/renard.h +++ b/src/base/math/renard.h @@ -18,7 +18,7 @@ class Renard [[nodiscard]] double floor(double value) const; private: - std::span numbers; + std::span numbers{}; }; } diff --git a/src/chart/generator/axis.cpp b/src/chart/generator/axis.cpp index 6768fb57f..2c77fd51e 100644 --- a/src/chart/generator/axis.cpp +++ b/src/chart/generator/axis.cpp @@ -1,12 +1,10 @@ #include "axis.h" -#include <__compare/compare_three_way.h> #include #include #include #include #include -#include #include #include #include @@ -14,6 +12,7 @@ #include #include #include +#include #include "base/alg/merge.h" #include "base/anim/interpolated.h" @@ -338,9 +337,6 @@ DimensionAxis interpolate(const DimensionAxis &op0, const Val &rhs, const Val::first_type *key = nullptr) -> Val { - printf("%s to %s\n", - DimensionAxis::mergedLabels(lhs.first).c_str(), - DimensionAxis::mergedLabels(rhs.first).c_str()); return {key ? *key : lhs.first, interpolate(lhs.second, rhs.second, factor)}; }; diff --git a/src/chart/generator/axis.h b/src/chart/generator/axis.h index fd331e895..a346c1099 100644 --- a/src/chart/generator/axis.h +++ b/src/chart/generator/axis.h @@ -188,13 +188,13 @@ struct DimensionAxis [[nodiscard]] static std::string mergedLabels( const std::vector &slices) { - return {std::from_range, + return std::ranges::to( std::views::transform(slices, [](const Data::SliceIndex &slice) { return ", " + slice.*which; }) - | std::views::join | std::views::drop(2)}; + | std::views::join | std::views::drop(2)); } private: diff --git a/src/chart/generator/plotbuilder.cpp b/src/chart/generator/plotbuilder.cpp index 3c71a4447..66fe6bcd6 100644 --- a/src/chart/generator/plotbuilder.cpp +++ b/src/chart/generator/plotbuilder.cpp @@ -462,14 +462,14 @@ void PlotBuilder::calcLegendAndLabel(const Data::DataTable &dataTable) if (auto &&series = plot->getOptions()->labelSeries(type); !series.empty() && isAutoTitle && calcLegend.dimension.empty()) - calcLegend.title = std::string{std::from_range, + calcLegend.title = std::ranges::to( series - | std::views::transform( - [](const Data::SeriesIndex &series) - { - return ", " + series.getColIndex(); - }) - | std::views::join | std::views::drop(2)}; + | std::views::transform( + [](const Data::SeriesIndex &series) + { + return ", " + series.getColIndex(); + }) + | std::views::join | std::views::drop(2)); } } @@ -571,14 +571,14 @@ void PlotBuilder::calcAxis(const Data::DataTable &dataTable, if (auto &&series = plot->getOptions()->labelSeries(type); !axis.dimension.setLabels(axisProps.step.getValue(1.0)) && !series.empty() && isAutoTitle) - axis.title = std::string{std::from_range, + axis.title = std::ranges::to( series - | std::views::transform( - [](const Data::SeriesIndex &series) - { - return ", " + series.getColIndex(); - }) - | std::views::join | std::views::drop(2)}; + | std::views::transform( + [](const Data::SeriesIndex &series) + { + return ", " + series.getColIndex(); + }) + | std::views::join | std::views::drop(2)); for (std::uint32_t pos{}; DimensionAxis::Item & i : axis.dimension.sortedItems()) i.endPos = i.startPos = diff --git a/src/chart/options/channel.h b/src/chart/options/channel.h index 82b60df39..9bfe20f2e 100644 --- a/src/chart/options/channel.h +++ b/src/chart/options/channel.h @@ -195,8 +195,18 @@ struct Channel [[nodiscard]] auto operator<=>( const LabelLevelList &) const = default; - LabelLevelList(std::size_t one) : levels{one} {} - template LabelLevelList(T &&) = delete; + explicit LabelLevelList(std::size_t one) : levels{one} {} + LabelLevelList(const LabelLevelList &) = default; + LabelLevelList(LabelLevelList &&) noexcept = default; + LabelLevelList &operator=(const LabelLevelList &) = default; + LabelLevelList &operator=( + LabelLevelList &&) noexcept = default; + + template + requires(!std::is_same_v, + LabelLevelList>) + // NOLINTNEXTLINE(cppcoreguidelines-missing-std-forward) + explicit LabelLevelList(T &&) = delete; explicit LabelLevelList(std::vector &&levels) : levels(std::move(levels)) @@ -204,8 +214,9 @@ struct Channel static LabelLevelList fromString(const std::string &str) { - return LabelLevelList{{std::from_range, - std::string_view{str} | std::views::split(',') + return LabelLevelList{ + std::ranges::to>( + std::string_view{str} | std::views::split(',') | std::views::transform( [](const auto &s) { @@ -222,7 +233,7 @@ struct Channel range.end(), ix); return ix; - })}}; + }))}; } [[nodiscard]] std::string toString() const diff --git a/src/chart/options/options.cpp b/src/chart/options/options.cpp index f720b5cb7..584826362 100644 --- a/src/chart/options/options.cpp +++ b/src/chart/options/options.cpp @@ -1,6 +1,7 @@ #include "options.h" +#include #include #include #include diff --git a/src/chart/options/options.h b/src/chart/options/options.h index 60279a8f3..f25f76af8 100644 --- a/src/chart/options/options.h +++ b/src/chart/options/options.h @@ -107,10 +107,10 @@ class Options : public OptionProperties } else if (!lll && !hasMeasure && ch.dimensions().size() > 1 && channel == legend.get().getValueOrAuto()) { - lll.emplace( - Channel::LabelLevelList{std::vector{std::from_range, + lll.emplace(Channel::LabelLevelList{ + std::ranges::to>( std::views::iota(std::size_t{}, - ch.dimensions().size())}}); + ch.dimensions().size()))}); } else if (!lll) { lll.emplace(std::size_t{0}); @@ -159,7 +159,9 @@ class Options : public OptionProperties } if (auto &&measure = ch.measure(); measure - && ch.labelLevel.getValue(std::size_t{}).levels + && ch.labelLevel + .getValue(Channel::LabelLevelList{size_t{}}) + .levels == std::vector{std::size_t{}}) return {*measure}; return {}; diff --git a/src/chart/rendering/drawaxes.cpp b/src/chart/rendering/drawaxes.cpp index 6b59e793f..7beea472c 100644 --- a/src/chart/rendering/drawaxes.cpp +++ b/src/chart/rendering/drawaxes.cpp @@ -28,7 +28,6 @@ #include "chart/main/events.h" #include "chart/main/style.h" #include "chart/options/channel.h" -#include "dataframe/old/types.h" #include "drawguides.h" #include "drawinterlacing.h" diff --git a/src/chart/rendering/drawlegend.cpp b/src/chart/rendering/drawlegend.cpp index 0fd1983d4..51f8707ba 100644 --- a/src/chart/rendering/drawlegend.cpp +++ b/src/chart/rendering/drawlegend.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "base/anim/interpolated.h" #include "base/geom/affinetransform.h" diff --git a/test/e2e/test_cases/test_cases.json b/test/e2e/test_cases/test_cases.json index 263d68f3c..10c3b3941 100644 --- a/test/e2e/test_cases/test_cases.json +++ b/test/e2e/test_cases/test_cases.json @@ -56,7 +56,7 @@ "refs": ["bc224eb"] }, "basic_animations/legend_transitions/color_2discrete_anim": { - "refs": ["86f503c"] + "refs": ["71a45d2"] }, "basic_animations/legend_transitions/color_conti_anim": { "refs": ["106245c"] @@ -80,7 +80,7 @@ "refs": ["0617129"] }, "basic_animations/legend_transitions/lightness_2discrete_anim": { - "refs": ["3e29b58"] + "refs": ["f818429"] }, "basic_animations/legend_transitions/lightness_conti_anim": { "refs": ["e65d2ff"] @@ -299,7 +299,7 @@ "refs": ["b627ce9"] }, "operations/orientation_tutorial_data/line_orientation": { - "refs": ["dc9ac6c"] + "refs": ["156df8d"] }, "operations/orientation_tutorial_data/rectangle_orientation": { "refs": ["a97572f"] From d8903483ee5f4f2921120ec2ed7b2d8156ab8bf7 Mon Sep 17 00:00:00 2001 From: Bela Schaum Date: Sun, 21 Sep 2025 09:06:54 +0200 Subject: [PATCH 3/4] rename to multiLevelAxis + clang-format --- src/chart/generator/plotbuilder.cpp | 8 ++++---- src/chart/main/style.cpp | 4 ++-- src/chart/main/style.h | 4 ++-- src/chart/options/channel.h | 4 +--- src/chart/rendering/drawaxes.cpp | 11 ++++++----- src/chart/rendering/drawaxes.h | 2 ++ 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/chart/generator/plotbuilder.cpp b/src/chart/generator/plotbuilder.cpp index 66fe6bcd6..3e3da0351 100644 --- a/src/chart/generator/plotbuilder.cpp +++ b/src/chart/generator/plotbuilder.cpp @@ -161,9 +161,9 @@ std::vector PlotBuilder::sortedBuckets( 0.0, !(marker.*buckets.marker_id_get).label.empty() ? std::make_optional( - DimensionAxis::mergedLabels( - (marker.*buckets.marker_id_get) - .label)) + DimensionAxis::mergedLabels( + (marker.*buckets.marker_id_get) + .label)) : std::nullopt); it->size += marker.size.getCoord( @@ -511,7 +511,7 @@ void PlotBuilder::calcLegendAndLabel(const Data::DataTable &dataTable) markerLabelsUnitPercent ? "%" : dataTable.get_series_info(meas->getColIndex(), - "unit")}}, + "unit")}}, ::Anim::String{meas->getColIndex()}}; } } diff --git a/src/chart/main/style.cpp b/src/chart/main/style.cpp index 1e505cfa4..113f532a7 100644 --- a/src/chart/main/style.cpp +++ b/src/chart/main/style.cpp @@ -266,7 +266,7 @@ Chart Chart::def() { .position = Anim::Interpolated(AxisLabel::Position::min_edge), .side = Anim::Interpolated(AxisLabel::Side::negative), - .multiAxis = Anim::Interpolated(AxisLabel::MultiAxis::split) + .multiLevelAxis = Anim::Interpolated(AxisLabel::MultiLevelAxis::flattened) } }, .ticks = { @@ -359,7 +359,7 @@ Chart Chart::def() { .position = Anim::Interpolated(AxisLabel::Position::min_edge), .side = Anim::Interpolated(AxisLabel::Side::negative), - .multiAxis = Anim::Interpolated(AxisLabel::MultiAxis::split) + .multiLevelAxis = Anim::Interpolated(AxisLabel::MultiLevelAxis::flattened) } }, .ticks = { diff --git a/src/chart/main/style.h b/src/chart/main/style.h index e72be1aa1..a0ade1af7 100644 --- a/src/chart/main/style.h +++ b/src/chart/main/style.h @@ -201,11 +201,11 @@ struct AxisLabelParams enum class Side : std::uint8_t { positive, negative }; - enum class MultiAxis : std::uint8_t { split, separate }; + enum class MultiLevelAxis : std::uint8_t { flattened, nested }; Param<::Anim::Interpolated> position; Param<::Anim::Interpolated> side; - Param<::Anim::Interpolated> multiAxis; + Param<::Anim::Interpolated> multiLevelAxis; }; struct AxisLabel : OrientedLabel, AxisLabelParams diff --git a/src/chart/options/channel.h b/src/chart/options/channel.h index 9bfe20f2e..f9689ebeb 100644 --- a/src/chart/options/channel.h +++ b/src/chart/options/channel.h @@ -264,9 +264,7 @@ struct Channel template concept ChannelIdLike = requires(const T &v) { - { - +v - } -> std::same_as; + { +v } -> std::same_as; }; template diff --git a/src/chart/rendering/drawaxes.cpp b/src/chart/rendering/drawaxes.cpp index 7beea472c..d9b0c72d7 100644 --- a/src/chart/rendering/drawaxes.cpp +++ b/src/chart/rendering/drawaxes.cpp @@ -466,15 +466,16 @@ void DrawAxes::drawTitle(Gen::AxisId axisIndex, -std::numbers::pi / 2.0 * (fades == ::Anim::second ? !isHorizontal( - titleStyle.orientation->get_or_first(index) - .value) + titleStyle.orientation->get_or_first(index) + .value) : titleStyle.orientation->factor( - Styles::AxisTitle::Orientation::vertical)); + Styles::AxisTitle::Orientation::vertical)); auto orientedSize = fades == ::Anim::second ? calcOrientation( - titleStyle.orientation->get_or_first(index).value) + titleStyle.orientation->get_or_first(index) + .value) : titleStyle.orientation->combine(calcOrientation); auto center = offset * (orientedSize / 2.0); @@ -577,7 +578,7 @@ void DrawAxes::drawDimensionLabel(Gen::AxisId axisIndex, ? labelStyle.side->get_or_first(index).value == Styles::AxisLabel::Side::negative : labelStyle.side->factor( - Styles::AxisLabel::Side::negative); + Styles::AxisLabel::Side::negative); auto draw = [&, posDir = coordSys diff --git a/src/chart/rendering/drawaxes.h b/src/chart/rendering/drawaxes.h index 17f875194..77bb40894 100644 --- a/src/chart/rendering/drawaxes.h +++ b/src/chart/rendering/drawaxes.h @@ -69,6 +69,8 @@ class DrawAxes : public DrawingContext Gen::SplitAxis::Parts::const_iterator, std::ranges::subrange_kind::sized>> splits; + // Refl::EnumArray> + // multiLabelAxis; [[nodiscard]] auto getIntervals(Gen::AxisId axisIndex, const Math::Range<> &filter = {0.0, 1.0}) const From 3dce41eff2b6940533a7db26df59686fb9ebab96 Mon Sep 17 00:00:00 2001 From: Bela Schaum Date: Fri, 26 Sep 2025 21:04:53 +0200 Subject: [PATCH 4/4] Add multilabel --- test/e2e/tests/features.json | 3 ++ test/e2e/tests/features/multilabel.mjs | 52 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 test/e2e/tests/features/multilabel.mjs diff --git a/test/e2e/tests/features.json b/test/e2e/tests/features.json index 92f06d979..22562d61c 100644 --- a/test/e2e/tests/features.json +++ b/test/e2e/tests/features.json @@ -39,6 +39,9 @@ }, "data_input/big_category_indexed": { "refs": ["d8538cb"] + }, + "multilabel": { + "refs": ["6356a25"] } } } diff --git a/test/e2e/tests/features/multilabel.mjs b/test/e2e/tests/features/multilabel.mjs new file mode 100644 index 000000000..2d08f2877 --- /dev/null +++ b/test/e2e/tests/features/multilabel.mjs @@ -0,0 +1,52 @@ +const testSteps = [ + (chart) => + chart.animate({ + data: { + series: [ + { + name: 'Foo', + values: ['Ted', 'Alice', 'Bob', 'Ted', 'Alice', 'Bob', 'Ted'] + }, + { name: 'Foo2', values: ['A', 'A', 'A', 'B', 'B', 'B', 'A'] }, + { name: 'Foo3', values: ['X', 'Y', 'Z', 'X', 'Y', 'Z', 'Y'] }, + { name: 'Bar', values: [23, 32, 12, 15, 41, 31, 1] } + ] + } + }), + (chart) => + chart.animate({ + x: ['Foo', 'Foo2'], + y: 'Bar' + }), + (chart) => + chart.animate({ + x: { labelLevel: 1 } + }), + (chart) => + chart.animate({ + x: { labelLevel: '[0, 1]' } + }), + (chart) => + chart.animate({ + x: { labelLevel: '[1, 0]' } + }), + (chart) => + chart.animate({ + color: ['Foo', 'Foo2'], + legend: 'color' + }), + (chart) => + chart.animate({ + color: { labelLevel: 1 } + }), + (chart) => + chart.animate({ + color: { labelLevel: '[0, 1]' } + }), + (chart) => + chart.animate({ + color: { labelLevel: '[1, 0]' } + }) +] + +export default testSteps