diff --git a/CHANGELOG.md b/CHANGELOG.md index 16f37534e..982ca4724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,14 @@ ### Changed -- Separate Channel properties to AxisChannel properties at config. - Channels 'set' rewrite doesn't clear AxisChannel properties. ### Added +- Separate Channel properties to AxisChannel properties at config. +- Move split align sort and reverse to AxisChannel - Add new sorting strategy: 'byLabel'. +- Enable split and align on mainAxis. ## [0.16.0] - 2024-11-28 diff --git a/src/apps/weblib/typeschema-api/config.yaml b/src/apps/weblib/typeschema-api/config.yaml index 0ba5df68d..3a03113bf 100644 --- a/src/apps/weblib/typeschema-api/config.yaml +++ b/src/apps/weblib/typeschema-api/config.yaml @@ -124,15 +124,12 @@ definitions: type: boolean align: description: | - Sets the alignment of the markers with relation to the main-axis only depending - on where the measure is. In case both axes have measures on them, this is determined - by the `orientation` of the chart. On sub-axis this settings has no effect. + Sets the alignment of the markers along the axis. type: string enum: [none, center, stretch] split: description: | - If set to true, markers will be split by the dimension(s) along the main-axis. - On sub-axis this settings has no effect. + If set to true, markers will be split by the dimension(s) along the axis. type: boolean Channels: diff --git a/src/chart/generator/plotbuilder.cpp b/src/chart/generator/plotbuilder.cpp index 1a2d4649b..3bb7d67cc 100644 --- a/src/chart/generator/plotbuilder.cpp +++ b/src/chart/generator/plotbuilder.cpp @@ -48,29 +48,35 @@ PlotBuilder::PlotBuilder(const Data::DataTable &dataTable, initDimensionTrackers(); std::size_t mainBucketSize{}; - auto &&subBuckets = generateMarkers(mainBucketSize); + std::size_t subBucketSize{}; + auto &&buckets = generateMarkers(mainBucketSize, subBucketSize); if (!plot->getOptions()->getChannels().anyAxisSet()) { - addSpecLayout(subBuckets); + addSpecLayout(buckets); normalizeSizes(); } else { normalizeSizes(); - addAxisLayout(subBuckets, mainBucketSize, dataTable); + addAxisLayout(buckets, + mainBucketSize, + subBucketSize, + dataTable); } normalizeColors(); calcLegendAndLabel(dataTable); } -void PlotBuilder::addAxisLayout(Buckets &subBuckets, +void PlotBuilder::addAxisLayout(Buckets &buckets, const std::size_t &mainBucketSize, + const std::size_t &subBucketSize, const Data::DataTable &dataTable) { - linkMarkers(subBuckets); - addSeparation(subBuckets, mainBucketSize); + linkMarkers(buckets, mainBucketSize, subBucketSize); calcAxises(dataTable); - addAlignment(subBuckets); + addAlignment(buckets, plot->getOptions()->subAxisType()); + addAlignment(buckets.sort(&Marker::mainId), + plot->getOptions()->mainAxisType()); } void PlotBuilder::initDimensionTrackers() @@ -82,7 +88,8 @@ void PlotBuilder::initDimensionTrackers() dataCube.combinedSizeOf(ch.dimensions()).second); } -Buckets PlotBuilder::generateMarkers(std::size_t &mainBucketSize) +Buckets PlotBuilder::generateMarkers(std::size_t &mainBucketSize, + std::size_t &subBucketSize) { const auto &mainIds(plot->getOptions()->mainAxis().dimensions()); auto subIds(plot->getOptions()->subAxis().dimensions()); @@ -91,6 +98,7 @@ Buckets PlotBuilder::generateMarkers(std::size_t &mainBucketSize) subIds.split_by(mainIds); mainBucketSize = dataCube.combinedSizeOf(mainIds).first; + subBucketSize = dataCube.combinedSizeOf(subIds).first; plot->markers.reserve(dataCube.df->get_record_count()); } @@ -201,13 +209,21 @@ void PlotBuilder::addSpecLayout(Buckets &buckets) } } -void PlotBuilder::linkMarkers(Buckets &buckets) +void PlotBuilder::linkMarkers(Buckets &buckets, + const std::size_t &mainBucketSize, + const std::size_t &subBucketSize) { auto &&hasMarkerConnection = linkMarkers(buckets.sort(&Marker::mainId), plot->getOptions()->mainAxisType()); + addSeparation(buckets, + plot->getOptions()->mainAxisType(), + subBucketSize); std::ignore = linkMarkers(buckets.sort(&Marker::subId), plot->getOptions()->subAxisType()); + addSeparation(buckets, + plot->getOptions()->subAxisType(), + mainBucketSize); if (hasMarkerConnection && plot->getOptions()->geometry.get() == ShapeType::line @@ -493,67 +509,62 @@ void PlotBuilder::calcAxis(const Data::DataTable &dataTable, } } -void PlotBuilder::addAlignment(const Buckets &subBuckets) const +void PlotBuilder::addAlignment(const Buckets &buckets, + AxisId axisIndex) const { - if (plot->getOptions()->isSplit()) return; + if (plot->getOptions()->isSplit(axisIndex)) return; - auto &subAxisRange = - plot->axises.at(plot->getOptions()->subAxisType()) - .measure.range; - if (std::signbit(subAxisRange.min) - || std::signbit(subAxisRange.max)) + auto &axisRange = plot->axises.at(axisIndex).measure.range; + if (std::signbit(axisRange.min) || std::signbit(axisRange.max)) return; const auto &axisProps = - plot->getOptions()->getChannels().axisPropsAt( - plot->getOptions()->subAxisType()); + plot->getOptions()->getChannels().axisPropsAt(axisIndex); if (axisProps.align == Base::Align::Type::none) return; if (axisProps.align == Base::Align::Type::center) { - auto &&halfSize = subAxisRange.size() / 2.0; + auto &&halfSize = axisRange.size() / 2.0; if (!Math::Floating::is_zero(halfSize)) - subAxisRange = {subAxisRange.min - halfSize, - subAxisRange.max - halfSize}; + axisRange = {axisRange.min - halfSize, + axisRange.max - halfSize}; } - auto &&subAxis = plot->getOptions()->subAxisType(); const Base::Align align{axisProps.align, {0.0, 1.0}}; - for (auto &&bucket : subBuckets) { + for (auto &&bucket : buckets) { Math::Range<> range; for (auto &&[marker, idx] : bucket) if (marker.enabled) - range.include(marker.getSizeBy(subAxis)); + range.include(marker.getSizeBy(axisIndex)); auto &&transform = align.getAligned(range) / range; for (auto &&[marker, idx] : bucket) - marker.setSizeBy(subAxis, - marker.getSizeBy(subAxis) * transform); + marker.setSizeBy(axisIndex, + marker.getSizeBy(axisIndex) * transform); } } -void PlotBuilder::addSeparation(const Buckets &subBuckets, - const std::size_t &mainBucketSize) const +void PlotBuilder::addSeparation(const Buckets &buckets, + AxisId axisIndex, + const std::size_t &otherBucketSize) const { - if (!plot->getOptions()->isSplit()) return; + if (!plot->getOptions()->isSplit(axisIndex)) return; const auto &axisProps = - plot->getOptions()->getChannels().axisPropsAt( - plot->getOptions()->subAxisType()); + plot->getOptions()->getChannels().axisPropsAt(axisIndex); auto align = axisProps.align; - std::vector ranges{mainBucketSize, Math::Range<>{{}, {}}}; - std::vector anyEnabled(mainBucketSize); + std::vector ranges{otherBucketSize, Math::Range<>{{}, {}}}; + std::vector anyEnabled(otherBucketSize); - auto &&subAxis = plot->getOptions()->subAxisType(); - for (auto &&bucket : subBuckets) + for (auto &&bucket : buckets) for (std::size_t i{}, prIx{}; auto &&[marker, idx] : bucket) { if (!marker.enabled) continue; (i += idx.itemId - std::exchange(prIx, idx.itemId)) %= ranges.size(); - ranges[i].include(marker.getSizeBy(subAxis).size()); + ranges[i].include(marker.getSizeBy(axisIndex).size()); anyEnabled[i] = true; } @@ -562,21 +573,20 @@ void PlotBuilder::addSeparation(const Buckets &subBuckets, if (anyEnabled[i]) max = max + ranges[i]; auto splitSpace = - plot->getStyle() - .plot.getAxis(plot->getOptions()->subAxisType()) - .spacing->get(max.max, plot->getStyle().calculatedSize()); + plot->getStyle().plot.getAxis(axisIndex).spacing->get(max.max, + plot->getStyle().calculatedSize()); for (auto i = 1U; i < ranges.size(); ++i) ranges[i] = ranges[i] + ranges[i - 1].max + (anyEnabled[i - 1] ? splitSpace : 0); - for (auto &&bucket : subBuckets) + for (auto &&bucket : buckets) for (std::size_t i{}, prIx{}; auto &&[marker, idx] : bucket) { (i += idx.itemId - std::exchange(prIx, idx.itemId)) %= ranges.size(); - marker.setSizeBy(subAxis, + marker.setSizeBy(axisIndex, Base::Align{align, ranges[i]}.getAligned( - marker.getSizeBy(subAxis))); + marker.getSizeBy(axisIndex))); } } diff --git a/src/chart/generator/plotbuilder.h b/src/chart/generator/plotbuilder.h index 15a6a924d..47f78d5cf 100644 --- a/src/chart/generator/plotbuilder.h +++ b/src/chart/generator/plotbuilder.h @@ -35,23 +35,28 @@ class PlotBuilder }; void initDimensionTrackers(); - Buckets generateMarkers(std::size_t &mainBucketSize); - void linkMarkers(Buckets &subBuckets); + Buckets generateMarkers(std::size_t &mainBucketSize, + std::size_t &subBucketSize); + void linkMarkers(Buckets &buckets, + const std::size_t &mainBucketSize, + const std::size_t &subBucketSize); [[nodiscard]] bool linkMarkers(const Buckets &buckets, AxisId axisIndex) const; void calcAxises(const Data::DataTable &dataTable); void calcLegendAndLabel(const Data::DataTable &dataTable); void calcAxis(const Data::DataTable &dataTable, AxisId type); - void addAlignment(const Buckets &subBuckets) const; - void addSeparation(const Buckets &subBuckets, - const std::size_t &mainBucketSize) const; + void addAlignment(const Buckets &buckets, AxisId axisIndex) const; + void addSeparation(const Buckets &buckets, + AxisId axisIndex, + const std::size_t &otherBucketSize) const; void normalizeSizes(); void normalizeColors(); [[nodiscard]] std::vector sortedBuckets(const Buckets &buckets, AxisId axisIndex) const; void addSpecLayout(Buckets &buckets); - void addAxisLayout(Buckets &subBuckets, + void addAxisLayout(Buckets &buckets, const std::size_t &mainBucketSize, + const std::size_t &subBucketSize, const Data::DataTable &dataTable); }; } diff --git a/src/chart/options/options.cpp b/src/chart/options/options.cpp index 275fbb579..ba5c5cdd1 100644 --- a/src/chart/options/options.cpp +++ b/src/chart/options/options.cpp @@ -91,10 +91,10 @@ std::optional Options::secondaryStackType() const return std::nullopt; } -bool Options::hasDimensionToSplit() const +bool Options::hasDimensionToSplit(AxisId at) const { - auto dims = subAxis().dimensions(); - dims.split_by(mainAxis().dimensions()); + auto dims = getChannels().at(at).dimensions(); + dims.split_by(getChannels().at(!at).dimensions()); return !dims.empty(); } @@ -111,7 +111,8 @@ Channels Options::shadowChannels() const &ch2 = shadow.at(ChannelId::noop); auto &&stacker : shadow.getDimensions({data(stackChannels), std::size_t{1} + secondary.has_value()})) { - if (stackChannelType() != subAxisType() || !isSplit()) + if (stackChannelType() != subAxisType() + || !isSplit(subAxisType())) ch1.removeSeries(stacker); ch2.removeSeries(stacker); } @@ -123,8 +124,9 @@ void Options::drilldownTo(const Options &other) { auto &stackChannel = this->stackChannel(); - if (!isSplit() || !other.isSplit()) - getChannels().axisPropsAt(subAxisType()).split = {}; + for (auto &&axis : Refl::enum_values()) + if (!isSplit(axis) || !other.isSplit(axis)) + getChannels().axisPropsAt(axis).split = {}; for (auto &&dim : other.getChannels().getDimensions()) if (!getChannels().isSeriesUsed(dim)) @@ -138,6 +140,7 @@ void Options::intersection(const Options &other) getChannels().removeSeries(dim); getChannels().axisPropsAt(subAxisType()).split = {}; + getChannels().axisPropsAt(mainAxisType()).split = {}; } bool Options::looksTheSame(const Options &other) const @@ -158,7 +161,7 @@ bool Options::looksTheSame(const Options &other) const void Options::simplify() { - if (isSplit()) return; + if (isSplit(subAxisType())) return; // remove all dimensions, only used at the end of stack auto &stackChannel = this->stackChannel(); @@ -197,7 +200,8 @@ bool Options::sameShadowAttribs(const Options &other) const return shape == shapeOther && coordSystem == other.coordSystem && angle == other.angle && orientation == other.orientation - && isSplit() == other.isSplit() + && isSplit(mainAxisType()) == other.isSplit(mainAxisType()) + && isSplit(subAxisType()) == other.isSplit(subAxisType()) && dataFilter == other.dataFilter; } diff --git a/src/chart/options/options.h b/src/chart/options/options.h index 02c949be9..c18abfca2 100644 --- a/src/chart/options/options.h +++ b/src/chart/options/options.h @@ -140,11 +140,11 @@ class Options : public OptionProperties return channels.at(stackChannelType()); } - [[nodiscard]] bool hasDimensionToSplit() const; - [[nodiscard]] bool isSplit() const + [[nodiscard]] bool hasDimensionToSplit(AxisId at) const; + [[nodiscard]] bool isSplit(AxisId byAxis) const { - return getChannels().axisPropsAt(subAxisType()).split - && hasDimensionToSplit(); + return getChannels().axisPropsAt(byAxis).split + && hasDimensionToSplit(byAxis); } Data::Filter dataFilter; std::optional tooltip; diff --git a/test/e2e/tests/config_tests/main_axis_split.mjs b/test/e2e/tests/config_tests/main_axis_split.mjs new file mode 100644 index 000000000..f9c52815e --- /dev/null +++ b/test/e2e/tests/config_tests/main_axis_split.mjs @@ -0,0 +1,30 @@ +const testSteps = [ + (chart) => { + const data = { + series: [ + { name: 'Foo', values: ['Alice', 'Bob', 'Ted'] }, + { name: 'Bar', values: ['Happy', 'Happy', 'Sad'] }, + { name: 'Baz', values: [1, 2, 3] }, + { name: 'Bau', values: [4, 3, 2] } + ] + } + + return chart.animate({ data }) + }, + (chart) => + chart.animate({ + config: { + x: { set: 'Foo', split: true }, + y: { set: 'Bar' } + } + }), + (chart) => + chart.animate({ + config: { + x: { set: 'Foo' }, + y: { set: ['Bar', 'Bau'] } + } + }) +] + +export default testSteps diff --git a/test/e2e/tests/fixes.json b/test/e2e/tests/fixes.json index 6b1ab02dc..578e58aa0 100644 --- a/test/e2e/tests/fixes.json +++ b/test/e2e/tests/fixes.json @@ -8,7 +8,7 @@ "refs": ["1732a49"] }, "143": { - "refs": ["95b9c83"] + "refs": ["0775a8d"] }, "144": { "refs": ["fde02e4"]