From 1bfa2e270a66a1e562672156147bcf80bcd644f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 26 Nov 2024 14:19:51 +0000
Subject: [PATCH 01/54] =?UTF-8?q?chore=20=F0=9F=A7=B9:=20add=20.vercel=20t?=
 =?UTF-8?q?o=20gitignore?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .gitignore | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index f5d5b316b..f2a8d647f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,5 @@ playwright-report
 
 app/public/storybook
 
-certificates
\ No newline at end of file
+certificates
+.vercel

From b1ea69a112b6c1a262a4c9b87b9a9557a03c557a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 26 Nov 2024 14:21:24 +0000
Subject: [PATCH 02/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20replicate=20?=
 =?UTF-8?q?column=20chart=20as=20bar=20chart?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-grouped-state-props.ts | 157 ++++++
 app/charts/bar/bars-grouped-state.tsx      | 471 ++++++++++++++++++
 app/charts/bar/bars-grouped.tsx            | 155 ++++++
 app/charts/bar/bars-stacked-state-props.ts | 168 +++++++
 app/charts/bar/bars-stacked-state.tsx      | 540 +++++++++++++++++++++
 app/charts/bar/bars-stacked.tsx            |  67 +++
 app/charts/bar/bars-state-props.ts         | 136 ++++++
 app/charts/bar/bars-state.tsx              | 309 ++++++++++++
 app/charts/bar/bars.tsx                    | 147 ++++++
 app/charts/bar/chart-bar.tsx               | 140 ++++++
 app/charts/bar/constants.ts                |   3 +
 app/charts/bar/overlay-bars.tsx            |  44 ++
 app/charts/bar/rendering-utils.ts          |  63 +++
 app/charts/chart-config-ui-options.ts      | 158 +++++-
 app/charts/index.ts                        | 172 ++++++-
 app/charts/shared/chart-state.ts           |  10 +-
 app/components/chart-with-filters.tsx      |  10 +
 app/config-types.ts                        |  96 +++-
 app/icons/index.tsx                        |   2 +
 19 files changed, 2830 insertions(+), 18 deletions(-)
 create mode 100644 app/charts/bar/bars-grouped-state-props.ts
 create mode 100644 app/charts/bar/bars-grouped-state.tsx
 create mode 100644 app/charts/bar/bars-grouped.tsx
 create mode 100644 app/charts/bar/bars-stacked-state-props.ts
 create mode 100644 app/charts/bar/bars-stacked-state.tsx
 create mode 100644 app/charts/bar/bars-stacked.tsx
 create mode 100644 app/charts/bar/bars-state-props.ts
 create mode 100644 app/charts/bar/bars-state.tsx
 create mode 100644 app/charts/bar/bars.tsx
 create mode 100644 app/charts/bar/chart-bar.tsx
 create mode 100644 app/charts/bar/constants.ts
 create mode 100644 app/charts/bar/overlay-bars.tsx
 create mode 100644 app/charts/bar/rendering-utils.ts

diff --git a/app/charts/bar/bars-grouped-state-props.ts b/app/charts/bar/bars-grouped-state-props.ts
new file mode 100644
index 000000000..a53dc865a
--- /dev/null
+++ b/app/charts/bar/bars-grouped-state-props.ts
@@ -0,0 +1,157 @@
+import { ascending, rollup, sum } from "d3-array";
+import orderBy from "lodash/orderBy";
+import { useCallback, useMemo } from "react";
+
+import { usePlottableData } from "@/charts/shared/chart-helpers";
+import {
+  BandXVariables,
+  BaseVariables,
+  ChartStateData,
+  InteractiveFiltersVariables,
+  NumericalYErrorVariables,
+  NumericalYVariables,
+  RenderingVariables,
+  SegmentVariables,
+  SortingVariables,
+  useBandXVariables,
+  useBaseVariables,
+  useChartData,
+  useInteractiveFiltersVariables,
+  useNumericalYErrorVariables,
+  useNumericalYVariables,
+  useSegmentVariables,
+} from "@/charts/shared/chart-state";
+import { useRenderingKeyVariable } from "@/charts/shared/rendering-utils";
+import { BarConfig, useChartConfigFilters } from "@/configurator";
+import { Observation, isTemporalEntityDimension } from "@/domain/data";
+import { sortByIndex } from "@/utils/array";
+
+import { ChartProps } from "../shared/ChartProps";
+
+export type BarsGroupedStateVariables = BaseVariables &
+  SortingVariables &
+  BandXVariables &
+  NumericalYVariables &
+  NumericalYErrorVariables &
+  SegmentVariables &
+  RenderingVariables &
+  InteractiveFiltersVariables;
+
+export const useBarsGroupedStateVariables = (
+  props: ChartProps<BarConfig>
+): BarsGroupedStateVariables => {
+  const {
+    chartConfig,
+    observations,
+    dimensions,
+    dimensionsById,
+    measures,
+    measuresById,
+  } = props;
+  const { fields, interactiveFiltersConfig } = chartConfig;
+  const { x, y, segment, animation } = fields;
+  const xDimension = dimensionsById[x.componentId];
+  const filters = useChartConfigFilters(chartConfig);
+
+  const baseVariables = useBaseVariables(chartConfig);
+  const bandXVariables = useBandXVariables(x, {
+    dimensionsById,
+    observations,
+  });
+  const numericalYVariables = useNumericalYVariables("bar", y, {
+    measuresById,
+  });
+  const numericalYErrorVariables = useNumericalYErrorVariables(y, {
+    numericalYVariables,
+    dimensions,
+    measures,
+  });
+  const segmentVariables = useSegmentVariables(segment, {
+    dimensionsById,
+    observations,
+  });
+  const interactiveFiltersVariables = useInteractiveFiltersVariables(
+    interactiveFiltersConfig,
+    { dimensionsById }
+  );
+
+  const { getX, getXAsDate } = bandXVariables;
+  const { getY } = numericalYVariables;
+  const sortData: BarsGroupedStateVariables["sortData"] = useCallback(
+    (data) => {
+      const { sortingOrder, sortingType } = x.sorting ?? {};
+      const xGetter = isTemporalEntityDimension(xDimension)
+        ? (d: Observation) => getXAsDate(d).getTime().toString()
+        : getX;
+      const order = [
+        ...rollup(
+          data,
+          (v) => sum(v, (d) => getY(d)),
+          (d) => xGetter(d)
+        ),
+      ]
+        .sort((a, b) => ascending(a[1], b[1]))
+        .map((d) => d[0]);
+
+      if (sortingType === "byDimensionLabel") {
+        return orderBy(data, xGetter, sortingOrder);
+      } else if (sortingType === "byMeasure") {
+        return sortByIndex({ data, order, getCategory: xGetter, sortingOrder });
+      } else {
+        return orderBy(data, xGetter, "asc");
+      }
+    },
+    [getX, getXAsDate, getY, x.sorting, xDimension]
+  );
+
+  const getRenderingKey = useRenderingKeyVariable(
+    dimensions,
+    filters,
+    interactiveFiltersConfig,
+    animation
+  );
+
+  return {
+    ...baseVariables,
+    sortData,
+    ...bandXVariables,
+    ...numericalYVariables,
+    ...numericalYErrorVariables,
+    ...segmentVariables,
+    ...interactiveFiltersVariables,
+    getRenderingKey,
+  };
+};
+
+export const useBarsGroupedStateData = (
+  chartProps: ChartProps<BarConfig>,
+  variables: BarsGroupedStateVariables
+): ChartStateData => {
+  const { chartConfig, observations } = chartProps;
+  const {
+    sortData,
+    xDimension,
+    getXAsDate,
+    getY,
+    getSegmentAbbreviationOrLabel,
+    getTimeRangeDate,
+  } = variables;
+  const plottableData = usePlottableData(observations, {
+    getY,
+  });
+  const sortedPlottableData = useMemo(() => {
+    return sortData(plottableData);
+  }, [sortData, plottableData]);
+  const data = useChartData(sortedPlottableData, {
+    chartConfig,
+    timeRangeDimensionId: xDimension.id,
+    getXAsDate,
+    getSegmentAbbreviationOrLabel,
+    getTimeRangeDate,
+  });
+
+  return {
+    ...data,
+    allData: sortedPlottableData,
+  };
+};
diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx
new file mode 100644
index 000000000..302346410
--- /dev/null
+++ b/app/charts/bar/bars-grouped-state.tsx
@@ -0,0 +1,471 @@
+import { extent, group, max, rollup, sum } from "d3-array";
+import {
+  ScaleBand,
+  scaleBand,
+  ScaleLinear,
+  scaleLinear,
+  ScaleOrdinal,
+  scaleOrdinal,
+  scaleTime,
+} from "d3-scale";
+import { schemeCategory10 } from "d3-scale-chromatic";
+import orderBy from "lodash/orderBy";
+import { useMemo } from "react";
+
+import {
+  BarsGroupedStateVariables,
+  useBarsGroupedStateData,
+  useBarsGroupedStateVariables,
+} from "@/charts/bar/bars-grouped-state-props";
+import {
+  PADDING_INNER,
+  PADDING_OUTER,
+  PADDING_WITHIN,
+} from "@/charts/bar/constants";
+import {
+  useAxisLabelHeightOffset,
+  useChartBounds,
+  useChartPadding,
+} from "@/charts/shared/chart-dimensions";
+import {
+  ChartContext,
+  ChartStateData,
+  CommonChartState,
+  InteractiveXTimeRangeState,
+} from "@/charts/shared/chart-state";
+import { TooltipInfo } from "@/charts/shared/interaction/tooltip";
+import {
+  getCenteredTooltipPlacement,
+  MOBILE_TOOLTIP_PLACEMENT,
+} from "@/charts/shared/interaction/tooltip-box";
+import useChartFormatters from "@/charts/shared/use-chart-formatters";
+import { InteractionProvider } from "@/charts/shared/use-interaction";
+import { useSize } from "@/charts/shared/use-size";
+import { BarConfig } from "@/configurator";
+import { Observation } from "@/domain/data";
+import { formatNumberWithUnit, useFormatNumber } from "@/formatters";
+import { getPalette } from "@/palettes";
+import { sortByIndex } from "@/utils/array";
+import {
+  getSortingOrders,
+  makeDimensionValueSorters,
+} from "@/utils/sorting-values";
+import { useIsMobile } from "@/utils/use-is-mobile";
+
+import { ChartProps } from "../shared/ChartProps";
+
+export type GroupedBarsState = CommonChartState &
+  BarsGroupedStateVariables &
+  InteractiveXTimeRangeState & {
+    chartType: "bar";
+    xScale: ScaleBand<string>;
+    xScaleInteraction: ScaleBand<string>;
+    xScaleIn: ScaleBand<string>;
+    yScale: ScaleLinear<number, number>;
+    segments: string[];
+    colors: ScaleOrdinal<string, string>;
+    getColorLabel: (segment: string) => string;
+    grouped: [string, Observation[]][];
+    getAnnotationInfo: (d: Observation) => TooltipInfo;
+  };
+
+const useBarsGroupedState = (
+  chartProps: ChartProps<BarConfig>,
+  variables: BarsGroupedStateVariables,
+  data: ChartStateData
+): GroupedBarsState => {
+  const { chartConfig } = chartProps;
+  const {
+    xDimension,
+    getX,
+    getXAsDate,
+    getXAbbreviationOrLabel,
+    getXLabel,
+    yMeasure,
+    getY,
+    getMinY,
+    showYStandardError,
+    yErrorMeasure,
+    getYError,
+    getYErrorRange,
+    segmentDimension,
+    segmentsByAbbreviationOrLabel,
+    getSegment,
+    getSegmentAbbreviationOrLabel,
+    getSegmentLabel,
+  } = variables;
+  const {
+    chartData,
+    scalesData,
+    segmentData,
+    timeRangeData,
+    paddingData,
+    allData,
+  } = data;
+  const { fields, interactiveFiltersConfig } = chartConfig;
+
+  const { width, height } = useSize();
+  const formatNumber = useFormatNumber({ decimals: "auto" });
+  const formatters = useChartFormatters(chartProps);
+
+  const segmentsByValue = useMemo(() => {
+    const values = segmentDimension?.values || [];
+
+    return new Map(values.map((d) => [d.value, d]));
+  }, [segmentDimension?.values]);
+
+  // segments
+  const segmentSortingOrder = fields.segment?.sorting?.sortingOrder;
+
+  const sumsBySegment = useMemo(() => {
+    return Object.fromEntries(
+      rollup(
+        segmentData,
+        (v) => sum(v, (x) => getY(x)),
+        (x) => getSegment(x)
+      )
+    );
+  }, [segmentData, getY, getSegment]);
+
+  const segmentFilter = segmentDimension?.id
+    ? chartConfig.cubes.find((d) => d.iri === segmentDimension.cubeIri)
+        ?.filters[segmentDimension.id]
+    : undefined;
+  const { allSegments, segments } = useMemo(() => {
+    const allUniqueSegments = Array.from(
+      new Set(segmentData.map((d) => getSegment(d)))
+    );
+    const uniqueSegments = Array.from(
+      new Set(scalesData.map((d) => getSegment(d)))
+    );
+    const sorting = fields?.segment?.sorting;
+    const sorters = makeDimensionValueSorters(segmentDimension, {
+      sorting,
+      sumsBySegment,
+      useAbbreviations: fields.segment?.useAbbreviations,
+      dimensionFilter: segmentFilter,
+    });
+    const allSegments = orderBy(
+      allUniqueSegments,
+      sorters,
+      getSortingOrders(sorters, sorting)
+    );
+
+    return {
+      allSegments,
+      segments: allSegments.filter((d) => uniqueSegments.includes(d)),
+    };
+  }, [
+    scalesData,
+    segmentData,
+    segmentDimension,
+    fields.segment?.sorting,
+    fields.segment?.useAbbreviations,
+    sumsBySegment,
+    segmentFilter,
+    getSegment,
+  ]);
+
+  /* Scales */
+  const xFilter = chartConfig.cubes.find((d) => d.iri === xDimension.cubeIri)
+    ?.filters[xDimension.id];
+  const sumsByX = useMemo(() => {
+    return Object.fromEntries(
+      rollup(
+        chartData,
+        (v) => sum(v, (x) => getY(x)),
+        (x) => getX(x)
+      )
+    );
+  }, [chartData, getX, getY]);
+
+  const {
+    xTimeRangeDomainLabels,
+    colors,
+    yScale,
+    paddingYScale,
+    xScaleTimeRange,
+    xScale,
+    xScaleIn,
+    xScaleInteraction,
+  } = useMemo(() => {
+    const colors = scaleOrdinal<string, string>();
+
+    if (fields.segment && segmentDimension && fields.segment.colorMapping) {
+      const orderedSegmentLabelsAndColors = allSegments.map((segment) => {
+        const dvIri =
+          segmentsByAbbreviationOrLabel.get(segment)?.value ||
+          segmentsByValue.get(segment)?.value ||
+          "";
+
+        return {
+          label: segment,
+          color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0],
+        };
+      });
+
+      colors.domain(orderedSegmentLabelsAndColors.map((s) => s.label));
+      colors.range(orderedSegmentLabelsAndColors.map((s) => s.color));
+    } else {
+      colors.domain(allSegments);
+      colors.range(getPalette(fields.segment?.palette));
+    }
+
+    colors.unknown(() => undefined);
+
+    const xValues = [...new Set(scalesData.map(getX))];
+    const xTimeRangeValues = [...new Set(timeRangeData.map(getX))];
+    const xSorting = fields.x?.sorting;
+    const xSorters = makeDimensionValueSorters(xDimension, {
+      sorting: xSorting,
+      useAbbreviations: fields.x?.useAbbreviations,
+      measureBySegment: sumsByX,
+      dimensionFilter: xFilter,
+    });
+    const xDomain = orderBy(
+      xValues,
+      xSorters,
+      getSortingOrders(xSorters, xSorting)
+    );
+    const xTimeRangeDomainLabels = xTimeRangeValues.map(getXLabel);
+    const xScale = scaleBand()
+      .domain(xDomain)
+      .paddingInner(PADDING_INNER)
+      .paddingOuter(PADDING_OUTER);
+    const xScaleInteraction = scaleBand()
+      .domain(xDomain)
+      .paddingInner(0)
+      .paddingOuter(0);
+    const xScaleIn = scaleBand().domain(segments).padding(PADDING_WITHIN);
+
+    const xScaleTimeRangeDomain = extent(timeRangeData, (d) =>
+      getXAsDate(d)
+    ) as [Date, Date];
+    const xScaleTimeRange = scaleTime().domain(xScaleTimeRangeDomain);
+
+    // y
+    const minValue = getMinY(scalesData, (d) =>
+      getYErrorRange ? getYErrorRange(d)[0] : getY(d)
+    );
+    const maxValue = Math.max(
+      max(scalesData, (d) =>
+        getYErrorRange ? getYErrorRange(d)[1] : getY(d)
+      ) ?? 0,
+      0
+    );
+    const yScale = scaleLinear().domain([minValue, maxValue]).nice();
+
+    const minPaddingValue = getMinY(paddingData, (d) =>
+      getYErrorRange ? getYErrorRange(d)[0] : getY(d)
+    );
+    const maxPaddingValue = Math.max(
+      max(paddingData, (d) =>
+        getYErrorRange ? getYErrorRange(d)[1] : getY(d)
+      ) ?? 0,
+      0
+    );
+    const paddingYScale = scaleLinear()
+      .domain([minPaddingValue, maxPaddingValue])
+      .nice();
+
+    return {
+      colors,
+      yScale,
+      paddingYScale,
+      xScaleTimeRange,
+      xScale,
+      xScaleIn,
+      xScaleInteraction,
+      xTimeRangeDomainLabels,
+    };
+  }, [
+    fields.segment,
+    fields.x?.sorting,
+    fields.x?.useAbbreviations,
+    segmentDimension,
+    scalesData,
+    getX,
+    xDimension,
+    sumsByX,
+    xFilter,
+    getXLabel,
+    segments,
+    timeRangeData,
+    paddingData,
+    allSegments,
+    segmentsByAbbreviationOrLabel,
+    segmentsByValue,
+    getXAsDate,
+    getYErrorRange,
+    getY,
+    getMinY,
+  ]);
+
+  // Group
+  const grouped: [string, Observation[]][] = useMemo(() => {
+    const xKeys = xScale.domain();
+    const groupedMap = group(chartData, getX);
+    const grouped: [string, Observation[]][] =
+      groupedMap.size < xKeys.length
+        ? xKeys.map((d) => {
+            if (groupedMap.has(d)) {
+              return [d, groupedMap.get(d) as Observation[]];
+            } else {
+              return [d, []];
+            }
+          })
+        : [...groupedMap];
+
+    return grouped.map(([key, data]) => {
+      return [
+        key,
+        sortByIndex({
+          data,
+          order: segments,
+          getCategory: getSegment,
+          sortingOrder: segmentSortingOrder,
+        }),
+      ];
+    });
+  }, [getSegment, getX, chartData, segmentSortingOrder, segments, xScale]);
+
+  const { left, bottom } = useChartPadding({
+    yScale: paddingYScale,
+    width,
+    height,
+    interactiveFiltersConfig,
+    animationPresent: !!fields.animation,
+    formatNumber,
+    bandDomain: xTimeRangeDomainLabels.every((d) => d === undefined)
+      ? xScale.domain()
+      : xTimeRangeDomainLabels,
+  });
+  const right = 40;
+  const { offset: yAxisLabelMargin } = useAxisLabelHeightOffset({
+    label: yMeasure.label,
+    width,
+    marginLeft: left,
+    marginRight: right,
+  });
+  const margins = {
+    top: 50 + yAxisLabelMargin,
+    right,
+    bottom,
+    left,
+  };
+  const bounds = useChartBounds(width, margins, height);
+  const { chartWidth, chartHeight } = bounds;
+
+  // Adjust of scales based on chart dimensions
+  xScale.range([0, chartWidth]);
+  xScaleInteraction.range([0, chartWidth]);
+  xScaleIn.range([0, xScale.bandwidth()]);
+  xScaleTimeRange.range([0, chartWidth]);
+  yScale.range([chartHeight, 0]);
+
+  const isMobile = useIsMobile();
+
+  // Tooltip
+  const getAnnotationInfo = (datum: Observation): TooltipInfo => {
+    const bw = xScale.bandwidth();
+    const x = getX(datum);
+
+    const tooltipValues = chartData.filter((d) => getX(d) === x);
+    const yValues = tooltipValues.map(getY);
+    const sortedTooltipValues = sortByIndex({
+      data: tooltipValues,
+      order: segments,
+      getCategory: getSegment,
+      // Always ascending to match visual order of colors of the stack
+      sortingOrder: "asc",
+    });
+    const yValueFormatter = (value: number | null) => {
+      return formatNumberWithUnit(
+        value,
+        formatters[yMeasure.id] ?? formatNumber,
+        yMeasure.unit
+      );
+    };
+
+    const xAnchorRaw = (xScale(x) as number) + bw * 0.5;
+    const [yMin, yMax] = extent(yValues, (d) => d ?? 0) as [number, number];
+    const yAnchor = isMobile ? chartHeight : yScale((yMin + yMax) * 0.5);
+    const placement = isMobile
+      ? MOBILE_TOOLTIP_PLACEMENT
+      : getCenteredTooltipPlacement({
+          chartWidth,
+          xAnchor: xAnchorRaw,
+          topAnchor: !fields.segment,
+        });
+
+    const getError = (d: Observation) => {
+      if (!showYStandardError || !getYError || getYError(d) == null) {
+        return;
+      }
+
+      return `${getYError(d)}${yErrorMeasure?.unit ?? ""}`;
+    };
+
+    return {
+      xAnchor: xAnchorRaw + (placement.x === "right" ? 0.5 : -0.5) * bw,
+      yAnchor,
+      placement,
+      xValue: getXAbbreviationOrLabel(datum),
+      datum: {
+        label: fields.segment && getSegmentAbbreviationOrLabel(datum),
+        value: yValueFormatter(getY(datum)),
+        error: getError(datum),
+        color: colors(getSegment(datum)) as string,
+      },
+      values: sortedTooltipValues.map((td) => ({
+        label: getSegmentAbbreviationOrLabel(td),
+        value: yMeasure.unit
+          ? `${formatNumber(getY(td))} ${yMeasure.unit}`
+          : formatNumber(getY(td)),
+        error: getError(td),
+        color: colors(getSegment(td)) as string,
+      })),
+    };
+  };
+
+  return {
+    chartType: "bar",
+    bounds,
+    chartData,
+    allData,
+    xScale,
+    xScaleInteraction,
+    xScaleIn,
+    xScaleTimeRange,
+    yScale,
+    segments,
+    colors,
+    getColorLabel: getSegmentLabel,
+    grouped,
+    getAnnotationInfo,
+    ...variables,
+  };
+};
+
+const GroupedBarChartProvider = (
+  props: React.PropsWithChildren<ChartProps<BarConfig>>
+) => {
+  const { children, ...chartProps } = props;
+  const variables = useBarsGroupedStateVariables(chartProps);
+  const data = useBarsGroupedStateData(chartProps, variables);
+  const state = useBarsGroupedState(chartProps, variables, data);
+
+  return (
+    <ChartContext.Provider value={state}>{children}</ChartContext.Provider>
+  );
+};
+
+export const GroupedBarChart = (
+  props: React.PropsWithChildren<ChartProps<BarConfig>>
+) => {
+  return (
+    <InteractionProvider>
+      <GroupedBarChartProvider {...props} />
+    </InteractionProvider>
+  );
+};
diff --git a/app/charts/bar/bars-grouped.tsx b/app/charts/bar/bars-grouped.tsx
new file mode 100644
index 000000000..c293fd57d
--- /dev/null
+++ b/app/charts/bar/bars-grouped.tsx
@@ -0,0 +1,155 @@
+import { useEffect, useMemo, useRef } from "react";
+
+import { GroupedBarsState } from "@/charts/bar/bars-grouped-state";
+import { RenderBarDatum, renderBars } from "@/charts/bar/rendering-utils";
+import { useChartState } from "@/charts/shared/chart-state";
+import {
+  RenderWhiskerDatum,
+  filterWithoutErrors,
+  renderContainer,
+  renderWhiskers,
+} from "@/charts/shared/rendering-utils";
+import { useTransitionStore } from "@/stores/transition";
+
+export const ErrorWhiskers = () => {
+  const {
+    bounds,
+    xScale,
+    xScaleIn,
+    getYErrorRange,
+    getYError,
+    yScale,
+    getSegment,
+    grouped,
+    showYStandardError,
+  } = useChartState() as GroupedBarsState;
+  const { margins, width, height } = bounds;
+  const ref = useRef<SVGGElement>(null);
+  const enableTransition = useTransitionStore((state) => state.enable);
+  const transitionDuration = useTransitionStore((state) => state.duration);
+  const renderData: RenderWhiskerDatum[] = useMemo(() => {
+    if (!getYErrorRange || !showYStandardError) {
+      return [];
+    }
+
+    const bandwidth = xScaleIn.bandwidth();
+    return grouped
+      .filter((d) => d[1].some(filterWithoutErrors(getYError)))
+      .flatMap(([segment, observations]) =>
+        observations.map((d) => {
+          const x0 = xScaleIn(getSegment(d)) as number;
+          const barWidth = Math.min(bandwidth, 15);
+          const [y1, y2] = getYErrorRange(d);
+          return {
+            key: `${segment}-${getSegment(d)}`,
+            x: (xScale(segment) as number) + x0 + bandwidth / 2 - barWidth / 2,
+            y1: yScale(y1),
+            y2: yScale(y2),
+            width: barWidth,
+          } as RenderWhiskerDatum;
+        })
+      );
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [
+    getSegment,
+    getYErrorRange,
+    getYError,
+    grouped,
+    showYStandardError,
+    xScale,
+    xScaleIn,
+    yScale,
+    width,
+    height,
+  ]);
+
+  useEffect(() => {
+    if (ref.current) {
+      renderContainer(ref.current, {
+        id: "bars-grouped-error-whiskers",
+        transform: `translate(${margins.left} ${margins.top})`,
+        transition: { enable: enableTransition, duration: transitionDuration },
+        render: (g, opts) => renderWhiskers(g, renderData, opts),
+      });
+    }
+  }, [
+    enableTransition,
+    margins.left,
+    margins.top,
+    renderData,
+    transitionDuration,
+  ]);
+
+  return <g ref={ref} />;
+};
+
+export const BarsGrouped = () => {
+  const {
+    bounds,
+    xScale,
+    xScaleIn,
+    getY,
+    yScale,
+    getSegment,
+    colors,
+    grouped,
+    getRenderingKey,
+  } = useChartState() as GroupedBarsState;
+  const ref = useRef<SVGGElement>(null);
+  const enableTransition = useTransitionStore((state) => state.enable);
+  const transitionDuration = useTransitionStore((state) => state.duration);
+  const { margins, height } = bounds;
+  const bandwidth = xScaleIn.bandwidth();
+  const y0 = yScale(0);
+  const renderData: RenderBarDatum[] = useMemo(() => {
+    return grouped.flatMap(([segment, observations]) => {
+      return observations.map((d) => {
+        const key = getRenderingKey(d, getSegment(d));
+        const x = getSegment(d);
+        const y = getY(d) ?? NaN;
+
+        return {
+          key,
+          x: (xScale(segment) as number) + (xScaleIn(x) as number),
+          y: yScale(Math.max(y, 0)),
+          width: bandwidth,
+          height: Math.max(0, Math.abs(yScale(y) - y0)),
+          color: colors(x),
+        };
+      });
+    });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [
+    colors,
+    getSegment,
+    bandwidth,
+    getY,
+    grouped,
+    xScaleIn,
+    xScale,
+    yScale,
+    y0,
+    getRenderingKey,
+    height,
+  ]);
+
+  useEffect(() => {
+    if (ref.current) {
+      renderContainer(ref.current, {
+        id: "bars-grouped",
+        transform: `translate(${margins.left} ${margins.top})`,
+        transition: { enable: enableTransition, duration: transitionDuration },
+        render: (g, opts) => renderBars(g, renderData, { ...opts, y0 }),
+      });
+    }
+  }, [
+    enableTransition,
+    margins.left,
+    margins.top,
+    renderData,
+    transitionDuration,
+    y0,
+  ]);
+
+  return <g ref={ref} />;
+};
diff --git a/app/charts/bar/bars-stacked-state-props.ts b/app/charts/bar/bars-stacked-state-props.ts
new file mode 100644
index 000000000..f8584cc91
--- /dev/null
+++ b/app/charts/bar/bars-stacked-state-props.ts
@@ -0,0 +1,168 @@
+import { ascending, descending, group } from "d3-array";
+import { useCallback, useMemo } from "react";
+
+import { getWideData, usePlottableData } from "@/charts/shared/chart-helpers";
+import {
+  BandXVariables,
+  BaseVariables,
+  ChartStateData,
+  InteractiveFiltersVariables,
+  NumericalYVariables,
+  RenderingVariables,
+  SegmentVariables,
+  SortingVariables,
+  useBandXVariables,
+  useBaseVariables,
+  useChartData,
+  useInteractiveFiltersVariables,
+  useNumericalYVariables,
+  useSegmentVariables,
+} from "@/charts/shared/chart-state";
+import { useRenderingKeyVariable } from "@/charts/shared/rendering-utils";
+import { BarConfig, useChartConfigFilters } from "@/configurator";
+import { Observation, isTemporalEntityDimension } from "@/domain/data";
+import { sortByIndex } from "@/utils/array";
+
+import { ChartProps } from "../shared/ChartProps";
+
+export type BarsStackedStateVariables = BaseVariables &
+  SortingVariables<{ plottableDataWide: Observation[] }> &
+  BandXVariables &
+  NumericalYVariables &
+  SegmentVariables &
+  RenderingVariables &
+  InteractiveFiltersVariables;
+
+export const useBarsStackedStateVariables = (
+  props: ChartProps<BarConfig>
+): BarsStackedStateVariables => {
+  const {
+    chartConfig,
+    observations,
+    dimensions,
+    dimensionsById,
+    measuresById,
+  } = props;
+  const { fields, interactiveFiltersConfig } = chartConfig;
+  const { x, y, segment, animation } = fields;
+  const xDimension = dimensionsById[x.componentId];
+  const filters = useChartConfigFilters(chartConfig);
+
+  const baseVariables = useBaseVariables(chartConfig);
+  const bandXVariables = useBandXVariables(x, {
+    dimensionsById,
+    observations,
+  });
+  const numericalYVariables = useNumericalYVariables("bar", y, {
+    measuresById,
+  });
+  const segmentVariables = useSegmentVariables(segment, {
+    dimensionsById,
+    observations,
+  });
+  const interactiveFiltersVariables = useInteractiveFiltersVariables(
+    interactiveFiltersConfig,
+    { dimensionsById }
+  );
+
+  const { getX, getXAsDate } = bandXVariables;
+  const sortData: BarsStackedStateVariables["sortData"] = useCallback(
+    (data, { plottableDataWide }) => {
+      const { sortingOrder, sortingType } = x.sorting ?? {};
+      const xGetter = isTemporalEntityDimension(xDimension)
+        ? (d: Observation) => getXAsDate(d).getTime().toString()
+        : getX;
+      const xOrder = plottableDataWide
+        .sort((a, b) => ascending(a.total ?? undefined, b.total ?? undefined))
+        .map(xGetter);
+
+      if (sortingOrder === "desc" && sortingType === "byDimensionLabel") {
+        return [...data].sort((a, b) => descending(xGetter(a), xGetter(b)));
+      } else if (sortingOrder === "asc" && sortingType === "byDimensionLabel") {
+        return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b)));
+      } else if (sortingType === "byMeasure") {
+        return sortByIndex({
+          data,
+          order: xOrder,
+          getCategory: xGetter,
+          sortingOrder,
+        });
+      } else {
+        return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b)));
+      }
+    },
+    [getX, getXAsDate, x.sorting, xDimension]
+  );
+
+  const getRenderingKey = useRenderingKeyVariable(
+    dimensions,
+    filters,
+    interactiveFiltersConfig,
+    animation
+  );
+
+  return {
+    ...baseVariables,
+    sortData,
+    ...bandXVariables,
+    ...numericalYVariables,
+    ...segmentVariables,
+    ...interactiveFiltersVariables,
+    getRenderingKey,
+  };
+};
+
+export type BarsStackedStateData = ChartStateData & {
+  plottableDataWide: Observation[];
+};
+
+export const useBarsStackedStateData = (
+  chartProps: ChartProps<BarConfig>,
+  variables: BarsStackedStateVariables
+): BarsStackedStateData => {
+  const { chartConfig, observations } = chartProps;
+  const { fields } = chartConfig;
+  const { x } = fields;
+  const {
+    sortData,
+    xDimension,
+    getX,
+    getXAsDate,
+    getY,
+    getSegment,
+    getSegmentAbbreviationOrLabel,
+    getTimeRangeDate,
+  } = variables;
+  const plottableData = usePlottableData(observations, {
+    getY,
+  });
+  const { sortedPlottableData, plottableDataWide } = useMemo(() => {
+    const plottableDataByX = group(plottableData, getX);
+    const plottableDataWide = getWideData({
+      dataGroupedByX: plottableDataByX,
+      xKey: x.componentId,
+      getY,
+      getSegment,
+    });
+
+    return {
+      sortedPlottableData: sortData(plottableData, {
+        plottableDataWide,
+      }),
+      plottableDataWide,
+    };
+  }, [plottableData, getX, x.componentId, getY, getSegment, sortData]);
+  const data = useChartData(sortedPlottableData, {
+    chartConfig,
+    timeRangeDimensionId: xDimension.id,
+    getXAsDate,
+    getSegmentAbbreviationOrLabel,
+    getTimeRangeDate,
+  });
+
+  return {
+    ...data,
+    allData: sortedPlottableData,
+    plottableDataWide,
+  };
+};
diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
new file mode 100644
index 000000000..d6d58ce24
--- /dev/null
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -0,0 +1,540 @@
+import { extent, group, rollup, sum } from "d3-array";
+import {
+  ScaleBand,
+  scaleBand,
+  ScaleLinear,
+  scaleLinear,
+  ScaleOrdinal,
+  scaleOrdinal,
+  scaleTime,
+} from "d3-scale";
+import { schemeCategory10 } from "d3-scale-chromatic";
+import {
+  stack,
+  stackOffsetDiverging,
+  stackOrderAscending,
+  stackOrderDescending,
+  stackOrderReverse,
+} from "d3-shape";
+import orderBy from "lodash/orderBy";
+import React, { useCallback, useMemo } from "react";
+
+import {
+  BarsStackedStateData,
+  BarsStackedStateVariables,
+  useBarsStackedStateData,
+  useBarsStackedStateVariables,
+} from "@/charts/bar/bars-stacked-state-props";
+import { PADDING_INNER, PADDING_OUTER } from "@/charts/bar/constants";
+import {
+  useAxisLabelHeightOffset,
+  useChartBounds,
+  useChartPadding,
+} from "@/charts/shared/chart-dimensions";
+import {
+  getWideData,
+  normalizeData,
+  useGetIdentityY,
+} from "@/charts/shared/chart-helpers";
+import {
+  ChartContext,
+  CommonChartState,
+  InteractiveXTimeRangeState,
+} from "@/charts/shared/chart-state";
+import { TooltipInfo } from "@/charts/shared/interaction/tooltip";
+import {
+  getCenteredTooltipPlacement,
+  MOBILE_TOOLTIP_PLACEMENT,
+} from "@/charts/shared/interaction/tooltip-box";
+import {
+  getStackedTooltipValueFormatter,
+  getStackedYScale,
+} from "@/charts/shared/stacked-helpers";
+import useChartFormatters from "@/charts/shared/use-chart-formatters";
+import { InteractionProvider } from "@/charts/shared/use-interaction";
+import { useSize } from "@/charts/shared/use-size";
+import { BarConfig } from "@/configurator";
+import { Observation } from "@/domain/data";
+import { useFormatNumber } from "@/formatters";
+import { getPalette } from "@/palettes";
+import { useChartInteractiveFilters } from "@/stores/interactive-filters";
+import { sortByIndex } from "@/utils/array";
+import {
+  getSortingOrders,
+  makeDimensionValueSorters,
+} from "@/utils/sorting-values";
+import { useIsMobile } from "@/utils/use-is-mobile";
+
+import { ChartProps } from "../shared/ChartProps";
+
+export type StackedBarsState = CommonChartState &
+  BarsStackedStateVariables &
+  InteractiveXTimeRangeState & {
+    chartType: "bar";
+    xScale: ScaleBand<string>;
+    xScaleInteraction: ScaleBand<string>;
+    yScale: ScaleLinear<number, number>;
+    segments: string[];
+    colors: ScaleOrdinal<string, string>;
+    getColorLabel: (segment: string) => string;
+    chartWideData: ArrayLike<Observation>;
+    series: $FixMe[];
+    getAnnotationInfo: (
+      d: Observation,
+      orderedSegments: string[]
+    ) => TooltipInfo;
+  };
+
+const useBarsStackedState = (
+  chartProps: ChartProps<BarConfig>,
+  variables: BarsStackedStateVariables,
+  data: BarsStackedStateData
+): StackedBarsState => {
+  const { chartConfig } = chartProps;
+  const {
+    xDimension,
+    getX,
+    getXAsDate,
+    getXAbbreviationOrLabel,
+    getXLabel,
+    yMeasure,
+    getY,
+    segmentDimension,
+    segmentsByAbbreviationOrLabel,
+    getSegment,
+    getSegmentAbbreviationOrLabel,
+    getSegmentLabel,
+  } = variables;
+  const getIdentityY = useGetIdentityY(yMeasure.id);
+  const {
+    chartData,
+    scalesData,
+    segmentData,
+    timeRangeData,
+    paddingData,
+    allData,
+  } = data;
+  const { fields, interactiveFiltersConfig } = chartConfig;
+
+  const { width, height } = useSize();
+  const formatNumber = useFormatNumber({ decimals: "auto" });
+  const formatters = useChartFormatters(chartProps);
+  const calculationType = useChartInteractiveFilters((d) => d.calculation.type);
+
+  const xKey = fields.x.componentId;
+
+  const segmentsByValue = useMemo(() => {
+    const values = segmentDimension?.values || [];
+
+    return new Map(values.map((d) => [d.value, d]));
+  }, [segmentDimension?.values]);
+
+  const sumsBySegment = useMemo(() => {
+    return Object.fromEntries(
+      rollup(
+        scalesData,
+        (v) => sum(v, (x) => getY(x)),
+        (x) => getSegment(x)
+      )
+    );
+  }, [getSegment, getY, scalesData]);
+
+  const segmentFilter = segmentDimension?.id
+    ? chartConfig.cubes.find((d) => d.iri === segmentDimension.cubeIri)
+        ?.filters[segmentDimension.id]
+    : undefined;
+  const { allSegments, segments } = useMemo(() => {
+    const allUniqueSegments = Array.from(new Set(segmentData.map(getSegment)));
+    const uniqueSegments = Array.from(new Set(scalesData.map(getSegment)));
+    const sorting = fields?.segment?.sorting;
+    const sorters = makeDimensionValueSorters(segmentDimension, {
+      sorting,
+      sumsBySegment,
+      useAbbreviations: fields.segment?.useAbbreviations,
+      dimensionFilter: segmentFilter,
+    });
+    const allSegments = orderBy(
+      allUniqueSegments,
+      sorters,
+      getSortingOrders(sorters, sorting)
+    );
+
+    return {
+      allSegments,
+      segments: allSegments.filter((d) => uniqueSegments.includes(d)),
+    };
+  }, [
+    scalesData,
+    segmentData,
+    segmentDimension,
+    fields.segment?.sorting,
+    fields.segment?.useAbbreviations,
+    sumsBySegment,
+    segmentFilter,
+    getSegment,
+  ]);
+
+  const sumsByX = useMemo(() => {
+    return Object.fromEntries(
+      rollup(
+        chartData,
+        (v) => sum(v, (x) => getY(x)),
+        (x) => getX(x)
+      )
+    );
+  }, [chartData, getX, getY]);
+
+  const normalize = calculationType === "percent";
+  const chartDataGroupedByX = useMemo(() => {
+    if (normalize) {
+      return group(
+        normalizeData(chartData, {
+          yKey: yMeasure.id,
+          getY,
+          getTotalGroupValue: (d) => sumsByX[getX(d)],
+        }),
+        getX
+      );
+    }
+
+    return group(chartData, getX);
+  }, [chartData, getX, sumsByX, getY, yMeasure.id, normalize]);
+
+  const chartWideData = useMemo(() => {
+    return getWideData({
+      dataGroupedByX: chartDataGroupedByX,
+      xKey,
+      getY,
+      getSegment,
+      allSegments: segments,
+      imputationType: "zeros",
+    });
+  }, [getSegment, getY, chartDataGroupedByX, segments, xKey]);
+
+  const xFilter = chartConfig.cubes.find((d) => d.iri === xDimension.cubeIri)
+    ?.filters[xDimension.id];
+
+  // Map ordered segments labels to colors
+  const {
+    colors,
+    xScale,
+    xTimeRangeDomainLabels,
+    xScaleInteraction,
+    xScaleTimeRange,
+  } = useMemo(() => {
+    const colors = scaleOrdinal<string, string>();
+
+    if (
+      fields.segment &&
+      segmentsByAbbreviationOrLabel &&
+      fields.segment.colorMapping
+    ) {
+      const orderedSegmentLabelsAndColors = allSegments.map((segment) => {
+        // FIXME: Labels in observations can differ from dimension values because the latter can be concatenated to only appear once per value
+        // See https://github.com/visualize-admin/visualization-tool/issues/97
+        const dvIri =
+          segmentsByAbbreviationOrLabel.get(segment)?.value ||
+          segmentsByValue.get(segment)?.value ||
+          "";
+
+        // There is no way to gracefully recover here :(
+        if (!dvIri) {
+          console.warn(`Can't find color for '${segment}'.`);
+        }
+
+        return {
+          label: segment,
+          color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0],
+        };
+      });
+
+      colors.domain(orderedSegmentLabelsAndColors.map((s) => s.label));
+      colors.range(orderedSegmentLabelsAndColors.map((s) => s.color));
+    } else {
+      colors.domain(allSegments);
+      colors.range(getPalette(fields.segment?.palette));
+    }
+
+    colors.unknown(() => undefined);
+
+    const xValues = [...new Set(scalesData.map(getX))];
+    const xTimeRangeValues = [...new Set(timeRangeData.map(getX))];
+    const xSorting = fields.x?.sorting;
+    const xSorters = makeDimensionValueSorters(xDimension, {
+      sorting: xSorting,
+      useAbbreviations: fields.x?.useAbbreviations,
+      measureBySegment: sumsByX,
+      dimensionFilter: xFilter,
+    });
+    const xDomain = orderBy(
+      xValues,
+      xSorters,
+      getSortingOrders(xSorters, xSorting)
+    );
+    const xTimeRangeDomainLabels = xTimeRangeValues.map(getXLabel);
+    const xScale = scaleBand()
+      .domain(xDomain)
+      .paddingInner(PADDING_INNER)
+      .paddingOuter(PADDING_OUTER);
+    const xScaleInteraction = scaleBand()
+      .domain(xDomain)
+      .paddingInner(0)
+      .paddingOuter(0);
+
+    const xScaleTimeRangeDomain = extent(timeRangeData, (d) =>
+      getXAsDate(d)
+    ) as [Date, Date];
+    const xScaleTimeRange = scaleTime().domain(xScaleTimeRangeDomain);
+
+    return {
+      colors,
+      xScale,
+      xTimeRangeDomainLabels,
+      xScaleTimeRange,
+      xScaleInteraction,
+    };
+  }, [
+    fields.segment,
+    fields.x.sorting,
+    fields.x.useAbbreviations,
+    xDimension,
+    xFilter,
+    sumsByX,
+    getX,
+    getXLabel,
+    getXAsDate,
+    scalesData,
+    timeRangeData,
+    segmentsByAbbreviationOrLabel,
+    segmentsByValue,
+    allSegments,
+  ]);
+
+  const animationIri = fields.animation?.componentId;
+  const getAnimation = useCallback(
+    (d: Observation) => {
+      return animationIri ? (d[animationIri] as string) : "";
+    },
+    [animationIri]
+  );
+
+  const yScale = useMemo(() => {
+    return getStackedYScale(scalesData, {
+      normalize,
+      getX,
+      getY,
+      getTime: getAnimation,
+    });
+  }, [scalesData, normalize, getX, getY, getAnimation]);
+
+  const paddingYScale = useMemo(() => {
+    //  When the user can toggle between absolute and relative values, we use the
+    // absolute values to calculate the yScale domain, so that the yScale doesn't
+    // change when the user toggles between absolute and relative values.
+    if (interactiveFiltersConfig?.calculation.active) {
+      const scale = getStackedYScale(paddingData, {
+        normalize: false,
+        getX,
+        getY,
+        getTime: getAnimation,
+      });
+
+      if (scale.domain()[1] < 100 && scale.domain()[0] > -100) {
+        return scaleLinear().domain([0, 100]);
+      }
+
+      return scale;
+    }
+
+    return getStackedYScale(paddingData, {
+      normalize,
+      getX,
+      getY,
+      getTime: getAnimation,
+    });
+  }, [
+    interactiveFiltersConfig?.calculation.active,
+    paddingData,
+    normalize,
+    getX,
+    getY,
+    getAnimation,
+  ]);
+
+  // stack order
+  const series = useMemo(() => {
+    const sorting = fields.segment?.sorting;
+    const sortingType = sorting?.sortingType;
+    const sortingOrder = sorting?.sortingOrder;
+    const stackOrder =
+      sortingType === "byTotalSize"
+        ? sortingOrder === "asc"
+          ? stackOrderAscending
+          : stackOrderDescending
+        : // Reverse segments here, so they're sorted from top to bottom
+          stackOrderReverse;
+
+    const stacked = stack()
+      .order(stackOrder)
+      .offset(stackOffsetDiverging)
+      .keys(segments);
+
+    return stacked(
+      chartWideData as {
+        [key: string]: number;
+      }[]
+    );
+  }, [chartWideData, fields.segment?.sorting, segments]);
+
+  /** Chart dimensions */
+  const { left, bottom } = useChartPadding({
+    yScale: paddingYScale,
+    width,
+    height,
+    interactiveFiltersConfig,
+    animationPresent: !!fields.animation,
+    formatNumber,
+    bandDomain: xTimeRangeDomainLabels.every((d) => d === undefined)
+      ? xScale.domain()
+      : xTimeRangeDomainLabels,
+    normalize,
+  });
+  const right = 40;
+  const { offset: yAxisLabelMargin } = useAxisLabelHeightOffset({
+    label: yMeasure.label,
+    width,
+    marginLeft: left,
+    marginRight: right,
+  });
+  const margins = {
+    top: 50 + yAxisLabelMargin,
+    right,
+    bottom,
+    left,
+  };
+  const bounds = useChartBounds(width, margins, height);
+  const { chartWidth, chartHeight } = bounds;
+
+  xScale.range([0, chartWidth]);
+  xScaleInteraction.range([0, chartWidth]);
+  xScaleTimeRange.range([0, chartWidth]);
+  yScale.range([chartHeight, 0]);
+
+  const isMobile = useIsMobile();
+
+  // Tooltips
+  const getAnnotationInfo = useCallback(
+    (datum: Observation): TooltipInfo => {
+      const bw = xScale.bandwidth();
+      const x = getX(datum);
+
+      const tooltipValues = chartDataGroupedByX.get(x) as Observation[];
+      const yValues = tooltipValues.map(getY);
+      const sortedTooltipValues = sortByIndex({
+        data: tooltipValues,
+        order: segments,
+        getCategory: getSegment,
+        sortingOrder: "asc",
+      });
+      const yValueFormatter = getStackedTooltipValueFormatter({
+        normalize,
+        yMeasureId: yMeasure.id,
+        yMeasureUnit: yMeasure.unit,
+        formatters,
+        formatNumber,
+      });
+
+      const xAnchorRaw = (xScale(x) as number) + bw * 0.5;
+      const yAnchor = isMobile
+        ? chartHeight
+        : yScale(sum(yValues.map((d) => d ?? 0)) * 0.5);
+      const placement = isMobile
+        ? MOBILE_TOOLTIP_PLACEMENT
+        : getCenteredTooltipPlacement({
+            chartWidth,
+            xAnchor: xAnchorRaw,
+            topAnchor: !fields.segment,
+          });
+
+      return {
+        xAnchor: xAnchorRaw + (placement.x === "right" ? 0.5 : -0.5) * bw,
+        yAnchor,
+        placement,
+        xValue: getXAbbreviationOrLabel(datum),
+        datum: {
+          label: fields.segment && getSegmentAbbreviationOrLabel(datum),
+          value: yValueFormatter(getY(datum), getIdentityY(datum)),
+          color: colors(getSegment(datum)) as string,
+        },
+        values: sortedTooltipValues.map((td) => ({
+          label: getSegmentAbbreviationOrLabel(td),
+          value: yValueFormatter(getY(td), getIdentityY(td)),
+          color: colors(getSegment(td)) as string,
+        })),
+      };
+    },
+    [
+      getX,
+      xScale,
+      chartDataGroupedByX,
+      segments,
+      getSegment,
+      yMeasure.id,
+      yMeasure.unit,
+      formatters,
+      formatNumber,
+      getXAbbreviationOrLabel,
+      fields.segment,
+      getSegmentAbbreviationOrLabel,
+      getY,
+      getIdentityY,
+      colors,
+      chartWidth,
+      chartHeight,
+      isMobile,
+      normalize,
+      yScale,
+    ]
+  );
+
+  return {
+    chartType: "bar",
+    bounds,
+    chartData,
+    allData,
+    xScale,
+    xScaleInteraction,
+    xScaleTimeRange,
+    yScale,
+    segments,
+    colors,
+    getColorLabel: getSegmentLabel,
+    chartWideData,
+    series,
+    getAnnotationInfo,
+    ...variables,
+  };
+};
+
+const StackedBarsChartProvider = (
+  props: React.PropsWithChildren<ChartProps<BarConfig>>
+) => {
+  const { children, ...chartProps } = props;
+  const variables = useBarsStackedStateVariables(chartProps);
+  const data = useBarsStackedStateData(chartProps, variables);
+  const state = useBarsStackedState(chartProps, variables, data);
+
+  return (
+    <ChartContext.Provider value={state}>{children}</ChartContext.Provider>
+  );
+};
+
+export const StackedBarsChart = (
+  props: React.PropsWithChildren<ChartProps<BarConfig>>
+) => {
+  return (
+    <InteractionProvider>
+      <StackedBarsChartProvider {...props} />
+    </InteractionProvider>
+  );
+};
diff --git a/app/charts/bar/bars-stacked.tsx b/app/charts/bar/bars-stacked.tsx
new file mode 100644
index 000000000..d7d5382c4
--- /dev/null
+++ b/app/charts/bar/bars-stacked.tsx
@@ -0,0 +1,67 @@
+import { useEffect, useMemo, useRef } from "react";
+
+import { StackedBarsState } from "@/charts/bar/bars-stacked-state";
+import { RenderBarDatum, renderBars } from "@/charts/bar/rendering-utils";
+import { useChartState } from "@/charts/shared/chart-state";
+import { renderContainer } from "@/charts/shared/rendering-utils";
+import { useTransitionStore } from "@/stores/transition";
+
+export const BarsStacked = () => {
+  const ref = useRef<SVGGElement>(null);
+  const enableTransition = useTransitionStore((state) => state.enable);
+  const transitionDuration = useTransitionStore((state) => state.duration);
+  const { bounds, getX, xScale, yScale, colors, series, getRenderingKey } =
+    useChartState() as StackedBarsState;
+  const { margins, height } = bounds;
+  const bandwidth = xScale.bandwidth();
+  const y0 = yScale(0);
+  const renderData: RenderBarDatum[] = useMemo(() => {
+    return series.flatMap((d) => {
+      const color = colors(d.key);
+
+      return d.map((segment: $FixMe) => {
+        const observation = segment.data;
+
+        return {
+          key: getRenderingKey(observation, d.key),
+          x: xScale(getX(observation)) as number,
+          y: yScale(segment[1]),
+          width: bandwidth,
+          height: Math.max(0, yScale(segment[0]) - yScale(segment[1])),
+          color,
+        };
+      });
+    });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [
+    bandwidth,
+    colors,
+    getX,
+    series,
+    xScale,
+    yScale,
+    getRenderingKey,
+    // Need to reset the yRange on height change
+    height,
+  ]);
+
+  useEffect(() => {
+    if (ref.current) {
+      renderContainer(ref.current, {
+        id: "bars-stacked",
+        transform: `translate(${margins.left} ${margins.top})`,
+        transition: { enable: enableTransition, duration: transitionDuration },
+        render: (g, opts) => renderBars(g, renderData, { ...opts, y0 }),
+      });
+    }
+  }, [
+    enableTransition,
+    margins.left,
+    margins.top,
+    renderData,
+    transitionDuration,
+    y0,
+  ]);
+
+  return <g ref={ref} />;
+};
diff --git a/app/charts/bar/bars-state-props.ts b/app/charts/bar/bars-state-props.ts
new file mode 100644
index 000000000..5859052b9
--- /dev/null
+++ b/app/charts/bar/bars-state-props.ts
@@ -0,0 +1,136 @@
+import { ascending, descending } from "d3-array";
+import { useCallback, useMemo } from "react";
+
+import { usePlottableData } from "@/charts/shared/chart-helpers";
+import {
+  BandXVariables,
+  BaseVariables,
+  ChartStateData,
+  InteractiveFiltersVariables,
+  NumericalYErrorVariables,
+  NumericalYVariables,
+  RenderingVariables,
+  SortingVariables,
+  useBandXVariables,
+  useBaseVariables,
+  useChartData,
+  useInteractiveFiltersVariables,
+  useNumericalYErrorVariables,
+  useNumericalYVariables,
+} from "@/charts/shared/chart-state";
+import { useRenderingKeyVariable } from "@/charts/shared/rendering-utils";
+import { BarConfig, useChartConfigFilters } from "@/configurator";
+import { isTemporalEntityDimension } from "@/domain/data";
+
+import { ChartProps } from "../shared/ChartProps";
+
+export type BarsStateVariables = BaseVariables &
+  SortingVariables &
+  BandXVariables &
+  NumericalYVariables &
+  NumericalYErrorVariables &
+  RenderingVariables &
+  InteractiveFiltersVariables;
+
+export const useBarsStateVariables = (
+  props: ChartProps<BarConfig>
+): BarsStateVariables => {
+  const {
+    chartConfig,
+    observations,
+    dimensions,
+    dimensionsById,
+    measures,
+    measuresById,
+  } = props;
+  const { fields, interactiveFiltersConfig } = chartConfig;
+  const { x, y, animation } = fields;
+  const xDimension = dimensionsById[x.componentId];
+  const filters = useChartConfigFilters(chartConfig);
+
+  const baseVariables = useBaseVariables(chartConfig);
+  const bandXVariables = useBandXVariables(x, {
+    dimensionsById,
+    observations,
+  });
+  const numericalYVariables = useNumericalYVariables("bar", y, {
+    measuresById,
+  });
+  const numericalYErrorVariables = useNumericalYErrorVariables(y, {
+    numericalYVariables,
+    dimensions,
+    measures,
+  });
+  const interactiveFiltersVariables = useInteractiveFiltersVariables(
+    interactiveFiltersConfig,
+    { dimensionsById }
+  );
+
+  const { getX, getXAsDate } = bandXVariables;
+  const { getY } = numericalYVariables;
+  const sortData: BarsStateVariables["sortData"] = useCallback(
+    (data) => {
+      const { sortingOrder, sortingType } = x.sorting ?? {};
+      const xGetter = isTemporalEntityDimension(xDimension) ? getXAsDate : getX;
+      if (sortingOrder === "desc" && sortingType === "byDimensionLabel") {
+        return [...data].sort((a, b) => descending(xGetter(a), xGetter(b)));
+      } else if (sortingOrder === "asc" && sortingType === "byDimensionLabel") {
+        return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b)));
+      } else if (sortingOrder === "desc" && sortingType === "byMeasure") {
+        return [...data].sort((a, b) =>
+          descending(getY(a) ?? -1, getY(b) ?? -1)
+        );
+      } else if (sortingOrder === "asc" && sortingType === "byMeasure") {
+        return [...data].sort((a, b) =>
+          ascending(getY(a) ?? -1, getY(b) ?? -1)
+        );
+      } else {
+        return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b)));
+      }
+    },
+    [getX, getXAsDate, getY, x.sorting, xDimension]
+  );
+
+  const getRenderingKey = useRenderingKeyVariable(
+    dimensions,
+    filters,
+    interactiveFiltersConfig,
+    animation
+  );
+
+  return {
+    ...baseVariables,
+    sortData,
+    ...bandXVariables,
+    ...numericalYVariables,
+    ...numericalYErrorVariables,
+    ...interactiveFiltersVariables,
+    getRenderingKey,
+  };
+};
+
+export const useBarsStateData = (
+  chartProps: ChartProps<BarConfig>,
+  variables: BarsStateVariables
+): ChartStateData => {
+  const { chartConfig, observations } = chartProps;
+  const { sortData, xDimension, getXAsDate, getY, getTimeRangeDate } =
+    variables;
+  const plottableData = usePlottableData(observations, {
+    getY,
+  });
+  const sortedPlottableData = useMemo(() => {
+    return sortData(plottableData);
+  }, [sortData, plottableData]);
+  const data = useChartData(sortedPlottableData, {
+    chartConfig,
+    timeRangeDimensionId: xDimension.id,
+    getXAsDate,
+    getTimeRangeDate,
+  });
+
+  return {
+    ...data,
+    allData: sortedPlottableData,
+  };
+};
diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
new file mode 100644
index 000000000..efd3dfb2e
--- /dev/null
+++ b/app/charts/bar/bars-state.tsx
@@ -0,0 +1,309 @@
+import { extent, max, rollup, sum } from "d3-array";
+import {
+  ScaleBand,
+  scaleBand,
+  ScaleLinear,
+  scaleLinear,
+  scaleTime,
+} from "d3-scale";
+import orderBy from "lodash/orderBy";
+import { useMemo } from "react";
+
+import {
+  BarsStateVariables,
+  useBarsStateData,
+  useBarsStateVariables,
+} from "@/charts/bar/bars-state-props";
+import { PADDING_INNER, PADDING_OUTER } from "@/charts/bar/constants";
+import {
+  useAxisLabelHeightOffset,
+  useChartBounds,
+  useChartPadding,
+} from "@/charts/shared/chart-dimensions";
+import {
+  ChartContext,
+  ChartStateData,
+  CommonChartState,
+  InteractiveXTimeRangeState,
+} from "@/charts/shared/chart-state";
+import { TooltipInfo } from "@/charts/shared/interaction/tooltip";
+import {
+  getCenteredTooltipPlacement,
+  MOBILE_TOOLTIP_PLACEMENT,
+} from "@/charts/shared/interaction/tooltip-box";
+import useChartFormatters from "@/charts/shared/use-chart-formatters";
+import { InteractionProvider } from "@/charts/shared/use-interaction";
+import { useSize } from "@/charts/shared/use-size";
+import { BarConfig } from "@/configurator";
+import { Observation } from "@/domain/data";
+import {
+  formatNumberWithUnit,
+  useFormatNumber,
+  useTimeFormatUnit,
+} from "@/formatters";
+import {
+  getSortingOrders,
+  makeDimensionValueSorters,
+} from "@/utils/sorting-values";
+import { useIsMobile } from "@/utils/use-is-mobile";
+
+import { ChartProps } from "../shared/ChartProps";
+
+export type BarsState = CommonChartState &
+  BarsStateVariables &
+  InteractiveXTimeRangeState & {
+    chartType: "bar";
+    xScale: ScaleBand<string>;
+    xScaleInteraction: ScaleBand<string>;
+    yScale: ScaleLinear<number, number>;
+    getAnnotationInfo: (d: Observation) => TooltipInfo;
+  };
+
+const useBarsState = (
+  chartProps: ChartProps<BarConfig>,
+  variables: BarsStateVariables,
+  data: ChartStateData
+): BarsState => {
+  const { chartConfig } = chartProps;
+  const {
+    xDimension,
+    getX,
+    getXAsDate,
+    getXAbbreviationOrLabel,
+    getXLabel,
+    xTimeUnit,
+    yMeasure,
+    getY,
+    getMinY,
+    showYStandardError,
+    yErrorMeasure,
+    getYError,
+    getYErrorRange,
+  } = variables;
+  const { chartData, scalesData, timeRangeData, paddingData, allData } = data;
+  const { fields, interactiveFiltersConfig } = chartConfig;
+
+  const { width, height } = useSize();
+  const formatNumber = useFormatNumber({ decimals: "auto" });
+  const formatters = useChartFormatters(chartProps);
+  const timeFormatUnit = useTimeFormatUnit();
+
+  const sumsByX = useMemo(() => {
+    return Object.fromEntries(
+      rollup(
+        chartData,
+        (v) => sum(v, (x) => getY(x)),
+        (x) => getX(x)
+      )
+    );
+  }, [chartData, getX, getY]);
+
+  const {
+    xScale,
+    yScale,
+    paddingYScale,
+    xScaleTimeRange,
+    xScaleInteraction,
+    xTimeRangeDomainLabels,
+  } = useMemo(() => {
+    const sorters = makeDimensionValueSorters(xDimension, {
+      sorting: fields.x.sorting,
+      measureBySegment: sumsByX,
+      useAbbreviations: fields.x.useAbbreviations,
+      dimensionFilter: xDimension?.id
+        ? chartConfig.cubes.find((d) => d.iri === xDimension.cubeIri)?.filters[
+            xDimension.id
+          ]
+        : undefined,
+    });
+    const sortingOrders = getSortingOrders(sorters, fields.x.sorting);
+    const bandDomain = orderBy(
+      [...new Set(scalesData.map(getX))],
+      sorters,
+      sortingOrders
+    );
+    const xTimeRangeValues = [...new Set(timeRangeData.map(getX))];
+    const xTimeRangeDomainLabels = xTimeRangeValues.map(getXLabel);
+    const xScale = scaleBand()
+      .domain(bandDomain)
+      .paddingInner(PADDING_INNER)
+      .paddingOuter(PADDING_OUTER);
+    const xScaleInteraction = scaleBand()
+      .domain(bandDomain)
+      .paddingInner(0)
+      .paddingOuter(0);
+
+    const xScaleTimeRangeDomain = extent(timeRangeData, (d) =>
+      getXAsDate(d)
+    ) as [Date, Date];
+
+    const xScaleTimeRange = scaleTime().domain(xScaleTimeRangeDomain);
+
+    const minValue = getMinY(scalesData, (d) =>
+      getYErrorRange ? getYErrorRange(d)[0] : getY(d)
+    );
+    const maxValue = Math.max(
+      max(scalesData, (d) =>
+        getYErrorRange ? getYErrorRange(d)[1] : getY(d)
+      ) ?? 0,
+      0
+    );
+    const yScale = scaleLinear().domain([minValue, maxValue]).nice();
+
+    const paddingMinValue = getMinY(paddingData, (d) =>
+      getYErrorRange ? getYErrorRange(d)[0] : getY(d)
+    );
+    const paddingMaxValue = Math.max(
+      max(paddingData, (d) =>
+        getYErrorRange ? getYErrorRange(d)[1] : getY(d)
+      ) ?? 0,
+      0
+    );
+    const paddingYScale = scaleLinear()
+      .domain([paddingMinValue, paddingMaxValue])
+      .nice();
+
+    return {
+      xScale,
+      yScale,
+      paddingYScale,
+      xScaleTimeRange,
+      xScaleInteraction,
+      xTimeRangeDomainLabels,
+    };
+  }, [
+    getX,
+    getXLabel,
+    getXAsDate,
+    getY,
+    getYErrorRange,
+    scalesData,
+    paddingData,
+    timeRangeData,
+    fields.x.sorting,
+    fields.x.useAbbreviations,
+    xDimension,
+    chartConfig.cubes,
+    sumsByX,
+    getMinY,
+  ]);
+
+  const { left, bottom } = useChartPadding({
+    yScale: paddingYScale,
+    width,
+    height,
+    interactiveFiltersConfig,
+    animationPresent: !!fields.animation,
+    formatNumber,
+    bandDomain: xTimeRangeDomainLabels.every((d) => d === undefined)
+      ? xScale.domain()
+      : xTimeRangeDomainLabels,
+  });
+  const right = 40;
+  const { offset: yAxisLabelMargin } = useAxisLabelHeightOffset({
+    label: yMeasure.label,
+    width,
+    marginLeft: left,
+    marginRight: right,
+  });
+  const margins = {
+    top: 50 + yAxisLabelMargin,
+    right,
+    bottom,
+    left,
+  };
+
+  const bounds = useChartBounds(width, margins, height);
+  const { chartWidth, chartHeight } = bounds;
+
+  xScale.range([0, chartWidth]);
+  xScaleInteraction.range([0, chartWidth]);
+  xScaleTimeRange.range([0, chartWidth]);
+  yScale.range([chartHeight, 0]);
+
+  const isMobile = useIsMobile();
+
+  // Tooltip
+  const getAnnotationInfo = (d: Observation): TooltipInfo => {
+    const xAnchor = (xScale(getX(d)) as number) + xScale.bandwidth() * 0.5;
+    const yAnchor = isMobile
+      ? chartHeight
+      : yScale(Math.max(getY(d) ?? NaN, 0));
+    const placement = isMobile
+      ? MOBILE_TOOLTIP_PLACEMENT
+      : getCenteredTooltipPlacement({
+          chartWidth,
+          xAnchor,
+          topAnchor: !fields.segment,
+        });
+
+    const xLabel = getXAbbreviationOrLabel(d);
+
+    const yValueFormatter = (value: number | null) =>
+      formatNumberWithUnit(
+        value,
+        formatters[yMeasure.id] ?? formatNumber,
+        yMeasure.unit
+      );
+
+    const getError = (d: Observation) => {
+      if (!showYStandardError || !getYError || getYError(d) === null) {
+        return;
+      }
+
+      return `${getYError(d)}${yErrorMeasure?.unit ?? ""}`;
+    };
+
+    const y = getY(d);
+
+    return {
+      xAnchor,
+      yAnchor,
+      placement,
+      xValue: xTimeUnit ? timeFormatUnit(xLabel, xTimeUnit) : xLabel,
+      datum: {
+        label: undefined,
+        value: y !== null && isNaN(y) ? "-" : `${yValueFormatter(getY(d))}`,
+        error: getError(d),
+        color: "",
+      },
+      values: undefined,
+    };
+  };
+
+  return {
+    chartType: "bar",
+    bounds,
+    chartData,
+    allData,
+    xScale,
+    xScaleTimeRange,
+    xScaleInteraction,
+    yScale,
+    getAnnotationInfo,
+    ...variables,
+  };
+};
+
+const BarChartProvider = (
+  props: React.PropsWithChildren<ChartProps<BarConfig>>
+) => {
+  const { children, ...chartProps } = props;
+  const variables = useBarsStateVariables(chartProps);
+  const data = useBarsStateData(chartProps, variables);
+  const state = useBarsState(chartProps, variables, data);
+
+  return (
+    <ChartContext.Provider value={state}>{children}</ChartContext.Provider>
+  );
+};
+
+export const BarChart = (
+  props: React.PropsWithChildren<ChartProps<BarConfig>>
+) => {
+  return (
+    <InteractionProvider>
+      <BarChartProvider {...props} />
+    </InteractionProvider>
+  );
+};
diff --git a/app/charts/bar/bars.tsx b/app/charts/bar/bars.tsx
new file mode 100644
index 000000000..810b30026
--- /dev/null
+++ b/app/charts/bar/bars.tsx
@@ -0,0 +1,147 @@
+import { schemeCategory10 } from "d3-scale-chromatic";
+import { useEffect, useMemo, useRef } from "react";
+
+import { BarsState } from "@/charts/bar/bars-state";
+import { RenderBarDatum, renderBars } from "@/charts/bar/rendering-utils";
+import { useChartState } from "@/charts/shared/chart-state";
+import {
+  RenderWhiskerDatum,
+  filterWithoutErrors,
+  renderContainer,
+  renderWhiskers,
+} from "@/charts/shared/rendering-utils";
+import { useTransitionStore } from "@/stores/transition";
+import { useTheme } from "@/themes";
+
+export const ErrorWhiskers = () => {
+  const {
+    getX,
+    getYError,
+    getYErrorRange,
+    chartData,
+    yScale,
+    xScale,
+    showYStandardError,
+    bounds,
+  } = useChartState() as BarsState;
+  const { margins, width, height } = bounds;
+  const ref = useRef<SVGGElement>(null);
+  const enableTransition = useTransitionStore((state) => state.enable);
+  const transitionDuration = useTransitionStore((state) => state.duration);
+  const renderData: RenderWhiskerDatum[] = useMemo(() => {
+    if (!getYErrorRange || !showYStandardError) {
+      return [];
+    }
+
+    const bandwidth = xScale.bandwidth();
+    return chartData.filter(filterWithoutErrors(getYError)).map((d, i) => {
+      const x0 = xScale(getX(d)) as number;
+      const barWidth = Math.min(bandwidth, 15);
+      const [y1, y2] = getYErrorRange(d);
+      return {
+        key: `${i}`,
+        x: x0 + bandwidth / 2 - barWidth / 2,
+        y1: yScale(y1),
+        y2: yScale(y2),
+        width: barWidth,
+      } as RenderWhiskerDatum;
+    });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [
+    chartData,
+    getX,
+    getYError,
+    getYErrorRange,
+    showYStandardError,
+    xScale,
+    yScale,
+    width,
+    height,
+  ]);
+
+  useEffect(() => {
+    if (ref.current) {
+      renderContainer(ref.current, {
+        id: "bars-error-whiskers",
+        transform: `translate(${margins.left} ${margins.top})`,
+        transition: { enable: enableTransition, duration: transitionDuration },
+        render: (g, opts) => renderWhiskers(g, renderData, opts),
+      });
+    }
+  }, [
+    enableTransition,
+    margins.left,
+    margins.top,
+    renderData,
+    transitionDuration,
+  ]);
+
+  return <g ref={ref} />;
+};
+
+export const Bars = () => {
+  const { chartData, bounds, getX, xScale, getY, yScale, getRenderingKey } =
+    useChartState() as BarsState;
+  const theme = useTheme();
+  const { margins } = bounds;
+  const ref = useRef<SVGGElement>(null);
+  const enableTransition = useTransitionStore((state) => state.enable);
+  const transitionDuration = useTransitionStore((state) => state.duration);
+  const bandwidth = xScale.bandwidth();
+  const y0 = yScale(0);
+  const renderData: RenderBarDatum[] = useMemo(() => {
+    const getColor = (d: number) => {
+      return d <= 0 ? theme.palette.secondary.main : schemeCategory10[0];
+    };
+
+    return chartData.map((d) => {
+      const key = getRenderingKey(d);
+      const xScaled = xScale(getX(d)) as number;
+      const yRaw = getY(d);
+      const y = yRaw === null || isNaN(yRaw) ? 0 : yRaw;
+      const yScaled = yScale(y);
+      const yRender = yScale(Math.max(y, 0));
+      const height = Math.max(0, Math.abs(yScaled - y0));
+      const color = getColor(y);
+
+      return {
+        key,
+        x: xScaled,
+        y: yRender,
+        width: bandwidth,
+        height,
+        color,
+      };
+    });
+  }, [
+    chartData,
+    bandwidth,
+    getX,
+    getY,
+    xScale,
+    yScale,
+    y0,
+    theme.palette.secondary.main,
+    getRenderingKey,
+  ]);
+
+  useEffect(() => {
+    if (ref.current) {
+      renderContainer(ref.current, {
+        id: "bars",
+        transform: `translate(${margins.left} ${margins.top})`,
+        transition: { enable: enableTransition, duration: transitionDuration },
+        render: (g, opts) => renderBars(g, renderData, { ...opts, y0 }),
+      });
+    }
+  }, [
+    enableTransition,
+    margins.left,
+    margins.top,
+    renderData,
+    transitionDuration,
+    y0,
+  ]);
+
+  return <g ref={ref} />;
+};
diff --git a/app/charts/bar/chart-bar.tsx b/app/charts/bar/chart-bar.tsx
new file mode 100644
index 000000000..215b657c2
--- /dev/null
+++ b/app/charts/bar/chart-bar.tsx
@@ -0,0 +1,140 @@
+import { memo } from "react";
+
+import { Bars, ErrorWhiskers } from "@/charts/bar/bars";
+import {
+  BarsGrouped,
+  ErrorWhiskers as ErrorWhiskersGrouped,
+} from "@/charts/bar/bars-grouped";
+import { GroupedBarChart } from "@/charts/bar/bars-grouped-state";
+import { BarsStacked } from "@/charts/bar/bars-stacked";
+import { StackedBarsChart } from "@/charts/bar/bars-stacked-state";
+import { BarChart } from "@/charts/bar/bars-state";
+import { InteractionBars } from "@/charts/bar/overlay-bars";
+import { ChartDataWrapper } from "@/charts/chart-data-wrapper";
+import { AxisHeightLinear } from "@/charts/shared/axis-height-linear";
+import {
+  AxisWidthBand,
+  AxisWidthBandDomain,
+} from "@/charts/shared/axis-width-band";
+import { BrushTime, shouldShowBrush } from "@/charts/shared/brush";
+import {
+  ChartContainer,
+  ChartControlsContainer,
+  ChartSvg,
+} from "@/charts/shared/containers";
+import { Tooltip } from "@/charts/shared/interaction/tooltip";
+import { LegendColor } from "@/charts/shared/legend-color";
+import { BarConfig, useChartConfigFilters } from "@/config-types";
+import { hasChartConfigs } from "@/configurator";
+import { TimeSlider } from "@/configurator/interactive-filters/time-slider";
+import { useConfiguratorState } from "@/src";
+
+import { ChartProps, VisualizationProps } from "../shared/ChartProps";
+
+export const ChartBarsVisualization = (
+  props: VisualizationProps<BarConfig>
+) => {
+  return <ChartDataWrapper {...props} Component={ChartBars} />;
+};
+
+const ChartBars = memo((props: ChartProps<BarConfig>) => {
+  const { chartConfig, dimensions } = props;
+  const { fields, interactiveFiltersConfig } = chartConfig;
+  const filters = useChartConfigFilters(chartConfig);
+  const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs);
+  const showTimeBrush = shouldShowBrush(
+    interactiveFiltersConfig,
+    dashboardFilters?.timeRange
+  );
+
+  return (
+    <>
+      {fields.segment?.componentId && fields.segment.type === "stacked" ? (
+        <StackedBarsChart {...props}>
+          <ChartContainer>
+            <ChartSvg>
+              <AxisHeightLinear />
+              <AxisWidthBand />
+              <BarsStacked />
+              <AxisWidthBandDomain />
+              <InteractionBars />
+              {showTimeBrush && <BrushTime />}
+            </ChartSvg>
+            <Tooltip type="multiple" />
+          </ChartContainer>
+          <ChartControlsContainer>
+            {fields.animation && (
+              <TimeSlider
+                filters={filters}
+                dimensions={dimensions}
+                {...fields.animation}
+              />
+            )}
+            <LegendColor
+              chartConfig={chartConfig}
+              symbol="square"
+              interactive={
+                fields.segment && interactiveFiltersConfig?.legend.active
+              }
+            />
+          </ChartControlsContainer>
+        </StackedBarsChart>
+      ) : fields.segment?.componentId && fields.segment.type === "grouped" ? (
+        <GroupedBarChart {...props}>
+          <ChartContainer>
+            <ChartSvg>
+              <AxisHeightLinear />
+              <AxisWidthBand />
+              <BarsGrouped />
+              <ErrorWhiskersGrouped />
+              <AxisWidthBandDomain />
+              <InteractionBars />
+              {showTimeBrush && <BrushTime />}
+            </ChartSvg>
+            <Tooltip type="multiple" />
+          </ChartContainer>
+          <ChartControlsContainer>
+            {fields.animation && (
+              <TimeSlider
+                filters={filters}
+                dimensions={dimensions}
+                {...fields.animation}
+              />
+            )}
+            <LegendColor
+              chartConfig={chartConfig}
+              symbol="square"
+              interactive={
+                fields.segment && interactiveFiltersConfig?.legend.active
+              }
+            />
+          </ChartControlsContainer>
+        </GroupedBarChart>
+      ) : (
+        <BarChart {...props}>
+          <ChartContainer>
+            <ChartSvg>
+              <AxisHeightLinear />
+              <AxisWidthBand />
+              <Bars />
+              <ErrorWhiskers />
+              <AxisWidthBandDomain />
+              <InteractionBars />
+              {showTimeBrush && <BrushTime />}
+            </ChartSvg>
+            <Tooltip type="single" />
+          </ChartContainer>
+          {fields.animation && (
+            <ChartControlsContainer>
+              <TimeSlider
+                filters={filters}
+                dimensions={dimensions}
+                {...fields.animation}
+              />
+            </ChartControlsContainer>
+          )}
+        </BarChart>
+      )}
+    </>
+  );
+});
diff --git a/app/charts/bar/constants.ts b/app/charts/bar/constants.ts
new file mode 100644
index 000000000..ecad19d48
--- /dev/null
+++ b/app/charts/bar/constants.ts
@@ -0,0 +1,3 @@
+export const PADDING_OUTER = 0;
+export const PADDING_INNER = 0.1;
+export const PADDING_WITHIN = 0.1;
diff --git a/app/charts/bar/overlay-bars.tsx b/app/charts/bar/overlay-bars.tsx
new file mode 100644
index 000000000..c919f2a41
--- /dev/null
+++ b/app/charts/bar/overlay-bars.tsx
@@ -0,0 +1,44 @@
+import { ColumnsState } from "@/charts/column/columns-state";
+import { ComboLineColumnState } from "@/charts/combo/combo-line-column-state";
+import { useChartState } from "@/charts/shared/chart-state";
+import { useInteraction } from "@/charts/shared/use-interaction";
+import { Observation } from "@/domain/data";
+
+export const InteractionBars = () => {
+  const [, dispatch] = useInteraction();
+
+  const { chartData, bounds, getX, xScaleInteraction } = useChartState() as
+    | ColumnsState
+    | ComboLineColumnState;
+  const { margins, chartHeight } = bounds;
+
+  const showTooltip = (d: Observation) => {
+    dispatch({
+      type: "INTERACTION_UPDATE",
+      value: { interaction: { visible: true, d } },
+    });
+  };
+  const hideTooltip = () => {
+    dispatch({
+      type: "INTERACTION_HIDE",
+    });
+  };
+  return (
+    <g transform={`translate(${margins.left} ${margins.top})`}>
+      {chartData.map((d, i) => (
+        <rect
+          key={i}
+          x={xScaleInteraction(getX(d)) as number}
+          y={0}
+          width={xScaleInteraction.bandwidth()}
+          height={Math.max(0, chartHeight)}
+          fill="hotpink"
+          fillOpacity={0}
+          stroke="none"
+          onMouseOut={hideTooltip}
+          onMouseOver={() => showTooltip(d)}
+        />
+      ))}
+    </g>
+  );
+};
diff --git a/app/charts/bar/rendering-utils.ts b/app/charts/bar/rendering-utils.ts
new file mode 100644
index 000000000..67123ec61
--- /dev/null
+++ b/app/charts/bar/rendering-utils.ts
@@ -0,0 +1,63 @@
+import { Selection } from "d3-selection";
+
+import {
+  RenderOptions,
+  maybeTransition,
+} from "@/charts/shared/rendering-utils";
+
+export type RenderBarDatum = {
+  key: string;
+  x: number;
+  y: number;
+  width: number;
+  height: number;
+  color: string;
+};
+
+type RenderBarOptions = RenderOptions & {
+  y0: number;
+};
+
+export const renderBars = (
+  g: Selection<SVGGElement, null, SVGGElement, unknown>,
+  data: RenderBarDatum[],
+  options: RenderBarOptions
+) => {
+  const { transition, y0 } = options;
+
+  g.selectAll<SVGRectElement, RenderBarDatum>("rect")
+    .data(data, (d) => d.key)
+    .join(
+      (enter) =>
+        enter
+          .append("rect")
+          .attr("data-index", (_, i) => i)
+          .attr("x", (d) => d.x)
+          .attr("y", y0)
+          .attr("width", (d) => d.width)
+          .attr("height", 0)
+          .attr("fill", (d) => d.color)
+          .call((enter) =>
+            maybeTransition(enter, {
+              transition,
+              s: (g) => g.attr("y", (d) => d.y).attr("height", (d) => d.height),
+            })
+          ),
+      (update) =>
+        maybeTransition(update, {
+          s: (g) =>
+            g
+              .attr("x", (d) => d.x)
+              .attr("y", (d) => d.y)
+              .attr("width", (d) => d.width)
+              .attr("height", (d) => d.height)
+              .attr("fill", (d) => d.color),
+          transition,
+        }),
+      (exit) =>
+        maybeTransition(exit, {
+          transition,
+          s: (g) => g.attr("y", y0).attr("height", 0).remove(),
+        })
+    );
+};
diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts
index 3122b1f3c..b4d544c1b 100644
--- a/app/charts/chart-config-ui-options.ts
+++ b/app/charts/chart-config-ui-options.ts
@@ -16,6 +16,7 @@ import {
 } from "@/charts/shared/chart-helpers";
 import {
   AreaConfig,
+  BarConfig,
   ChartConfig,
   ChartSubType,
   ChartType,
@@ -294,6 +295,7 @@ export interface ChartSpec<T extends ChartConfig = ChartConfig> {
 interface ChartSpecs {
   area: ChartSpec<AreaConfig>;
   column: ChartSpec<ColumnConfig>;
+  bar: ChartSpec<BarConfig>;
   line: ChartSpec<LineConfig>;
   map: ChartSpec<MapConfig>;
   pie: ChartSpec<PieConfig>;
@@ -351,6 +353,7 @@ const LINE_SEGMENT_SORTING: EncodingSortingOption<LineConfig>[] = [
 ];
 
 export const COLUMN_SEGMENT_SORTING = getDefaultSegmentSorting<ColumnConfig>();
+export const BAR_SEGMENT_SORTING = getDefaultSegmentSorting<BarConfig>();
 
 export const PIE_SEGMENT_SORTING: EncodingSortingOption<PieConfig>[] = [
   { sortingType: "byAuto", sortingOrder: ["asc", "desc"] },
@@ -359,7 +362,7 @@ export const PIE_SEGMENT_SORTING: EncodingSortingOption<PieConfig>[] = [
 ];
 
 export const ANIMATION_FIELD_SPEC: EncodingSpec<
-  ColumnConfig | MapConfig | ScatterPlotConfig | PieConfig
+  ColumnConfig | BarConfig | MapConfig | ScatterPlotConfig | PieConfig
 > = {
   field: "animation",
   optional: true,
@@ -473,6 +476,7 @@ export const disableStacked = (d?: Component): boolean => {
 export const defaultSegmentOnChange: OnEncodingChange<
   | AreaConfig
   | ColumnConfig
+  | BarConfig
   | LineConfig
   | ScatterPlotConfig
   | PieConfig
@@ -765,6 +769,158 @@ const chartConfigOptionsUISpec: ChartSpecs = {
     ],
     interactiveFilters: ["legend", "timeRange", "animation"],
   },
+  bar: {
+    chartType: "bar",
+    encodings: [
+      {
+        field: "y",
+        optional: false,
+        idAttributes: ["componentId"],
+        componentTypes: ["NumericalMeasure"],
+        filters: false,
+        onChange: (id, { chartConfig, measures }) => {
+          if (chartConfig.fields.segment?.type === "stacked") {
+            const yMeasure = measures.find((d) => d.id === id);
+
+            if (disableStacked(yMeasure)) {
+              setWith(chartConfig, "fields.segment.type", "grouped", Object);
+
+              if (chartConfig.interactiveFiltersConfig?.calculation) {
+                setWith(
+                  chartConfig,
+                  "interactiveFiltersConfig.calculation",
+                  { active: false, type: "identity" },
+                  Object
+                );
+              }
+            }
+          }
+        },
+        options: {
+          showStandardError: {},
+        },
+      },
+      {
+        field: "x",
+        optional: false,
+        idAttributes: ["componentId"],
+        componentTypes: [
+          "TemporalDimension",
+          "TemporalEntityDimension",
+          "TemporalOrdinalDimension",
+          "NominalDimension",
+          "OrdinalDimension",
+          "GeoCoordinatesDimension",
+          "GeoShapesDimension",
+        ],
+        filters: true,
+        sorting: [
+          { sortingType: "byAuto", sortingOrder: ["asc", "desc"] },
+          { sortingType: "byMeasure", sortingOrder: ["asc", "desc"] },
+          { sortingType: "byDimensionLabel", sortingOrder: ["asc", "desc"] },
+        ],
+        onChange: (id, { chartConfig, dimensions }) => {
+          const component = dimensions.find((d) => d.id === id);
+
+          if (!isTemporalDimension(component)) {
+            setWith(
+              chartConfig,
+              "interactiveFiltersConfig.timeRange.active",
+              false,
+              Object
+            );
+          }
+        },
+        options: {
+          useAbbreviations: {},
+        },
+      },
+      {
+        field: "segment",
+        optional: true,
+        idAttributes: ["componentId"],
+        componentTypes: SEGMENT_ENABLED_COMPONENTS,
+        filters: true,
+        sorting: BAR_SEGMENT_SORTING,
+        onChange: (id, options) => {
+          const { chartConfig, dimensions, measures } = options;
+          defaultSegmentOnChange(id, options);
+
+          const components = [...dimensions, ...measures];
+          const segment: ColumnSegmentField = get(
+            chartConfig,
+            "fields.segment"
+          );
+          const yComponent = components.find(
+            (d) => d.id === chartConfig.fields.y.componentId
+          );
+          setWith(
+            chartConfig,
+            "fields.segment",
+            {
+              ...segment,
+              type: disableStacked(yComponent) ? "grouped" : "stacked",
+            },
+            Object
+          );
+        },
+        options: {
+          calculation: {
+            getDisabledState: (chartConfig) => {
+              const grouped = chartConfig.fields.segment?.type === "grouped";
+
+              return {
+                disabled: grouped,
+                warnMessage: grouped
+                  ? t({
+                      id: "controls.calculation.disabled-by-grouped",
+                      message:
+                        "100% mode cannot be used with a grouped layout.",
+                    })
+                  : undefined,
+              };
+            },
+          },
+          chartSubType: {
+            getValues: (chartConfig, dimensions) => {
+              const yId = chartConfig.fields.y.componentId;
+              const yDimension = dimensions.find((d) => d.id === yId);
+              const disabledStacked = disableStacked(yDimension);
+
+              return [
+                {
+                  value: "stacked",
+                  disabled: disabledStacked,
+                  warnMessage: disabledStacked
+                    ? t({
+                        id: "controls.segment.stacked.disabled-by-scale-type",
+                        message:
+                          "Stacked layout can only be enabled if the vertical axis dimension has a ratio scale.",
+                      })
+                    : undefined,
+                },
+                {
+                  value: "grouped",
+                  disabled: false,
+                },
+              ];
+            },
+            onChange: (d, { chartConfig }) => {
+              if (chartConfig.interactiveFiltersConfig && d === "grouped") {
+                const path = "interactiveFiltersConfig.calculation";
+                setWith(chartConfig, path, { active: false, type: "identity" });
+              }
+            },
+          },
+          colorPalette: {},
+          useAbbreviations: {},
+        },
+      },
+      ANIMATION_FIELD_SPEC,
+    ],
+    interactiveFilters: ["legend", "timeRange", "animation"],
+  },
+
   line: {
     chartType: "line",
     encodings: [
diff --git a/app/charts/index.ts b/app/charts/index.ts
index 4439b187d..9e667d844 100644
--- a/app/charts/index.ts
+++ b/app/charts/index.ts
@@ -92,6 +92,7 @@ import { unreachableError } from "@/utils/unreachable";
 
 const chartTypes: ChartType[] = [
   "column",
+  "bar",
   "line",
   "area",
   "scatterplot",
@@ -105,6 +106,7 @@ const chartTypes: ChartType[] = [
 
 export const regularChartTypes: RegularChartType[] = [
   "column",
+  "bar",
   "line",
   "area",
   "scatterplot",
@@ -130,15 +132,16 @@ function getChartTypeOrder({ cubeCount }: { cubeCount: number }): ChartOrder {
   const multiCubeBoost = cubeCount > 1 ? -100 : 0;
   return {
     column: 0,
-    line: 1,
-    area: 2,
-    scatterplot: 3,
-    pie: 4,
-    map: 5,
-    table: 6,
-    comboLineSingle: 7 + multiCubeBoost,
-    comboLineDual: 8 + multiCubeBoost,
-    comboLineColumn: 9 + multiCubeBoost,
+    bar: 1,
+    line: 2,
+    area: 3,
+    scatterplot: 4,
+    pie: 5,
+    map: 6,
+    table: 7,
+    comboLineSingle: 8 + multiCubeBoost,
+    comboLineDual: 9 + multiCubeBoost,
+    comboLineColumn: 10 + multiCubeBoost,
   };
 }
 
@@ -423,6 +426,31 @@ export const getInitialConfig = (
           y: { componentId: numericalMeasures[0].id },
         },
       };
+
+    case "bar":
+      const barXComponentId = findPreferredDimension(
+        sortBy(dimensions, (d) => (isGeoDimension(d) ? 1 : -1)),
+        [
+          "TemporalDimension",
+          "TemporalEntityDimension",
+          "TemporalOrdinalDimension",
+        ]
+      ).id;
+
+      return {
+        ...getGenericConfigProps(),
+        chartType,
+        interactiveFiltersConfig: getInitialInteractiveFiltersConfig({
+          timeRangeComponentId: barXComponentId,
+        }),
+        fields: {
+          x: {
+            componentId: barXComponentId,
+            sorting: DEFAULT_SORTING,
+          },
+          y: { componentId: numericalMeasures[0].id },
+        },
+      };
     case "line":
       const lineXComponentId = temporalDimensions[0].id;
 
@@ -983,6 +1011,97 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
     },
     interactiveFiltersConfig: interactiveFiltersAdjusters,
   },
+  bar: {
+    cubes: ({ oldValue, newChartConfig }) => {
+      return produce(newChartConfig, (draft) => {
+        draft.cubes = oldValue;
+      });
+    },
+    fields: {
+      x: {
+        componentId: ({ oldValue, newChartConfig, dimensions }) => {
+          // When switching from a scatterplot, x is a measure.
+          if (dimensions.find((d) => d.id === oldValue)) {
+            return produce(newChartConfig, (draft) => {
+              draft.fields.x.componentId = oldValue;
+            });
+          }
+
+          return newChartConfig;
+        },
+      },
+      y: {
+        componentId: ({ oldValue, newChartConfig }) => {
+          return produce(newChartConfig, (draft) => {
+            draft.fields.y.componentId = oldValue;
+          });
+        },
+      },
+      segment: ({
+        oldValue,
+        oldChartConfig,
+        newChartConfig,
+        dimensions,
+        measures,
+      }) => {
+        let newSegment: ColumnSegmentField | undefined;
+        const yMeasure = measures.find(
+          (d) => d.id === newChartConfig.fields.y.componentId
+        );
+
+        // When switching from a table chart, a whole fields object is passed as oldValue.
+        if (oldChartConfig.chartType === "table") {
+          const tableSegment = convertTableFieldsToSegmentField({
+            fields: oldValue as TableFields,
+            dimensions,
+            measures,
+          });
+
+          if (tableSegment) {
+            newSegment = {
+              ...tableSegment,
+              sorting: DEFAULT_SORTING,
+              type: disableStacked(yMeasure) ? "grouped" : "stacked",
+            };
+          }
+          // Otherwise we are dealing with a segment field. We shouldn't take
+          // the segment from oldValue if the component has already been used as
+          // x axis.
+        } else if (
+          newChartConfig.fields.x.componentId !== oldValue.componentId
+        ) {
+          const oldSegment = oldValue as Exclude<typeof oldValue, TableFields>;
+          newSegment = {
+            ...oldSegment,
+            // We could encounter byMeasure sorting type (Pie chart); we should
+            // switch to byTotalSize sorting then.
+            sorting: adjustSegmentSorting({
+              segment: oldSegment,
+              acceptedValues: COLUMN_SEGMENT_SORTING.map((d) => d.sortingType),
+              defaultValue: "byTotalSize",
+            }),
+            type: disableStacked(yMeasure) ? "grouped" : "stacked",
+          };
+        }
+
+        return produce(newChartConfig, (draft) => {
+          if (newSegment) {
+            draft.fields.segment = newSegment;
+          }
+        });
+      },
+      animation: ({ oldValue, newChartConfig }) => {
+        return produce(newChartConfig, (draft) => {
+          // Temporal dimension could be used as X axis, in this case we need to
+          // remove the animation.
+          if (newChartConfig.fields.x.componentId !== oldValue?.componentId) {
+            draft.fields.animation = oldValue;
+          }
+        });
+      },
+    },
+    interactiveFiltersConfig: interactiveFiltersAdjusters,
+  },
   line: {
     cubes: ({ oldValue, newChartConfig }) => {
       return produce(newChartConfig, (draft) => {
@@ -1652,6 +1771,34 @@ const chartConfigsPathOverrides: {
       },
     },
   },
+  bar: {
+    map: {
+      "fields.areaLayer.componentId": { path: "fields.x.componentId" },
+      "fields.areaLayer.color.componentId": { path: "fields.y.componentId" },
+    },
+    table: {
+      fields: { path: "fields.segment" },
+    },
+    comboLineSingle: {
+      "fields.y.componentIds": {
+        path: "fields.y.componentId",
+        oldValue: (d: ComboLineSingleFields["y"]["componentIds"]) => d[0],
+      },
+    },
+    comboLineDual: {
+      "fields.y.leftAxisComponentId": { path: "fields.y.componentId" },
+    },
+    comboLineColumn: {
+      "fields.y": {
+        path: "fields.y.componentId",
+        oldValue: (d: ComboLineColumnFields["y"]) => {
+          return d.lineAxisOrientation === "left"
+            ? d.lineComponentId
+            : d.columnComponentId;
+        },
+      },
+    },
+  },
   line: {
     map: {
       "fields.areaLayer.color.componentId": { path: "fields.y.componentId" },
@@ -1944,10 +2091,10 @@ export const getPossibleChartTypes = ({
     (d) => isTemporalDimension(d) || isTemporalEntityDimension(d)
   );
 
-  const categoricalEnabled: RegularChartType[] = ["column", "pie"];
-  const geoEnabled: RegularChartType[] = ["column", "map", "pie"];
+  const categoricalEnabled: RegularChartType[] = ["column", "bar", "pie"];
+  const geoEnabled: RegularChartType[] = ["column", "bar", "map", "pie"];
   const multipleNumericalMeasuresEnabled: RegularChartType[] = ["scatterplot"];
-  const timeEnabled: RegularChartType[] = ["area", "column", "line"];
+  const timeEnabled: RegularChartType[] = ["area", "column", "bar", "line"];
 
   const possibles: ChartType[] = ["table"];
   if (numericalMeasures.length > 0) {
@@ -2076,6 +2223,7 @@ export const getChartSymbol = (
   switch (chartType) {
     case "area":
     case "column":
+    case "bar":
     case "comboLineColumn":
     case "pie":
     case "map":
diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts
index c116e74ed..871a9f369 100644
--- a/app/charts/shared/chart-state.ts
+++ b/app/charts/shared/chart-state.ts
@@ -5,6 +5,9 @@ import overEvery from "lodash/overEvery";
 import { createContext, useCallback, useContext, useMemo } from "react";
 
 import { AreasState } from "@/charts/area/areas-state";
+import { GroupedBarsState } from "@/charts/bar/bars-grouped-state";
+import { StackedBarsState } from "@/charts/bar/bars-stacked-state";
+import { BarsState } from "@/charts/bar/bars-state";
 import { GroupedColumnsState } from "@/charts/column/columns-grouped-state";
 import { StackedColumnsState } from "@/charts/column/columns-stacked-state";
 import { ColumnsState } from "@/charts/column/columns-state";
@@ -69,6 +72,9 @@ export type ChartState =
   | ColumnsState
   | StackedColumnsState
   | GroupedColumnsState
+  | BarsState
+  | StackedBarsState
+  | GroupedBarsState
   | ComboLineSingleState
   | ComboLineColumnState
   | ComboLineDualState
@@ -103,6 +109,7 @@ export const useChartState = () => {
 export type ChartWithInteractiveXTimeRangeState =
   | AreasState
   | ColumnsState
+  | BarsState
   | LinesState;
 
 export type NumericalValueGetter = (d: Observation) => number | null;
@@ -296,7 +303,7 @@ export type NumericalYVariables = {
 
 export const useNumericalYVariables = (
   // Combo charts have their own logic for y scales.
-  chartType: "area" | "column" | "line" | "pie" | "scatterplot",
+  chartType: "area" | "column" | "bar" | "line" | "pie" | "scatterplot",
   y: GenericField,
   { measuresById }: { measuresById: MeasuresById }
 ): NumericalYVariables => {
@@ -318,6 +325,7 @@ export const useNumericalYVariables = (
       switch (chartType) {
         case "area":
         case "column":
+        case "bar":
         case "pie":
           return Math.min(0, min(data, _getY) ?? 0);
         case "line":
diff --git a/app/components/chart-with-filters.tsx b/app/components/chart-with-filters.tsx
index 37240eb09..2740bf173 100644
--- a/app/components/chart-with-filters.tsx
+++ b/app/components/chart-with-filters.tsx
@@ -23,6 +23,12 @@ const ChartColumnsVisualization = dynamic(
     () => null as never
   )
 );
+const ChartBarsVisualization = dynamic(
+  import("@/charts/bar/chart-bar").then(
+    (mod) => mod.ChartBarsVisualization,
+    () => null as never
+  )
+);
 const ChartComboLineSingleVisualization = dynamic(
   import("@/charts/combo/chart-combo-line-single").then(
     (mod) => mod.ChartComboLineSingleVisualization,
@@ -98,6 +104,10 @@ const GenericChart = (props: GenericChartProps) => {
       return (
         <ChartColumnsVisualization {...commonProps} chartConfig={chartConfig} />
       );
+    case "bar":
+      return (
+        <ChartBarsVisualization {...commonProps} chartConfig={chartConfig} />
+      );
     case "line":
       return (
         <ChartLinesVisualization {...commonProps} chartConfig={chartConfig} />
diff --git a/app/config-types.ts b/app/config-types.ts
index e32cc88a1..635a77e40 100644
--- a/app/config-types.ts
+++ b/app/config-types.ts
@@ -307,6 +307,37 @@ const ColumnConfig = t.intersection([
 export type ColumnFields = t.TypeOf<typeof ColumnFields>;
 export type ColumnConfig = t.TypeOf<typeof ColumnConfig>;
 
+const BarSegmentField = t.intersection([
+  GenericSegmentField,
+  SortingField,
+  t.type({ type: ChartSubType }),
+]);
+export type BarSegmentField = t.TypeOf<typeof BarSegmentField>;
+
+const BarFields = t.intersection([
+  t.type({
+    x: t.intersection([GenericField, SortingField]),
+    y: GenericField,
+  }),
+  t.partial({
+    segment: BarSegmentField,
+    animation: AnimationField,
+  }),
+]);
+const BarConfig = t.intersection([
+  GenericChartConfig,
+  t.type(
+    {
+      chartType: t.literal("bar"),
+      interactiveFiltersConfig: InteractiveFiltersConfig,
+      fields: BarFields,
+    },
+    "BarConfig"
+  ),
+]);
+export type BarFields = t.TypeOf<typeof BarFields>;
+export type BarConfig = t.TypeOf<typeof BarConfig>;
+
 const LineSegmentField = t.intersection([GenericSegmentField, SortingField]);
 export type LineSegmentField = t.TypeOf<typeof LineSegmentField>;
 
@@ -726,9 +757,36 @@ const ComboLineColumnConfig = t.intersection([
 ]);
 export type ComboLineColumnConfig = t.TypeOf<typeof ComboLineColumnConfig>;
 
+const ComboLineBarFields = t.type({
+  x: GenericField,
+  y: t.type({
+    lineComponentId: t.string,
+    lineAxisOrientation: t.union([t.literal("left"), t.literal("right")]),
+    barComponentId: t.string,
+    palette: t.string,
+    colorMapping: ColorMapping,
+  }),
+});
+
+export type ComboLineBarFields = t.TypeOf<typeof ComboLineBarFields>;
+
+const ComboLineBarConfig = t.intersection([
+  GenericChartConfig,
+  t.type(
+    {
+      chartType: t.literal("comboLineBar"),
+      fields: ComboLineBarFields,
+      interactiveFiltersConfig: InteractiveFiltersConfig,
+    },
+    "ComboLineBarConfig"
+  ),
+]);
+export type ComboLineBarConfig = t.TypeOf<typeof ComboLineBarConfig>;
+
 export type ChartSegmentField =
   | AreaSegmentField
   | ColumnSegmentField
+  | BarSegmentField
   | LineSegmentField
   | PieSegmentField
   | ScatterPlotSegmentField;
@@ -736,6 +794,7 @@ export type ChartSegmentField =
 const RegularChartConfig = t.union([
   AreaConfig,
   ColumnConfig,
+  BarConfig,
   LineConfig,
   MapConfig,
   PieConfig,
@@ -795,6 +854,12 @@ export const isColumnConfig = (
   return chartConfig.chartType === "column";
 };
 
+export const isBarConfig = (
+  chartConfig: ChartConfig
+): chartConfig is BarConfig => {
+  return chartConfig.chartType === "bar";
+};
+
 export const isComboLineSingleConfig = (
   chartConfig: ChartConfig
 ): chartConfig is ComboLineSingleConfig => {
@@ -845,10 +910,13 @@ export const isMapConfig = (
 
 export const canBeNormalized = (
   chartConfig: ChartConfig
-): chartConfig is AreaConfig | ColumnConfig => {
+): chartConfig is AreaConfig | ColumnConfig | BarConfig => {
   return (
     chartConfig.chartType === "area" ||
     (chartConfig.chartType === "column" &&
+      chartConfig.fields.segment !== undefined &&
+      chartConfig.fields.segment.type === "stacked") ||
+    (chartConfig.chartType === "bar" &&
       chartConfig.fields.segment !== undefined &&
       chartConfig.fields.segment.type === "stacked")
   );
@@ -859,10 +927,11 @@ export const isSegmentInConfig = (
 ): chartConfig is
   | AreaConfig
   | ColumnConfig
+  | BarConfig
   | LineConfig
   | PieConfig
   | ScatterPlotConfig => {
-  return ["area", "column", "line", "pie", "scatterplot"].includes(
+  return ["area", "column", "bar", "line", "pie", "scatterplot"].includes(
     chartConfig.chartType
   );
 };
@@ -870,13 +939,15 @@ export const isSegmentInConfig = (
 export const isSortingInConfig = (
   chartConfig: ChartConfig
 ): chartConfig is AreaConfig | ColumnConfig | LineConfig | PieConfig => {
-  return ["area", "column", "line", "pie"].includes(chartConfig.chartType);
+  return ["area", "column", "bar", "line", "pie"].includes(
+    chartConfig.chartType
+  );
 };
 
 export const isAnimationInConfig = (
   chartConfig: ChartConfig
 ): chartConfig is ColumnConfig | MapConfig | PieConfig | ScatterPlotConfig => {
-  return ["column", "map", "pie", "scatterplot"].includes(
+  return ["column", "bar", "map", "pie", "scatterplot"].includes(
     chartConfig.chartType
   );
 };
@@ -952,6 +1023,22 @@ type ColumnAdjusters = BaseAdjusters<ColumnConfig> & {
   };
 };
 
+type BarAdjusters = BaseAdjusters<BarConfig> & {
+  fields: {
+    x: { componentId: FieldAdjuster<BarConfig, string> };
+    y: { componentId: FieldAdjuster<BarConfig, string> };
+    segment: FieldAdjuster<
+      BarConfig,
+      | LineSegmentField
+      | AreaSegmentField
+      | ScatterPlotSegmentField
+      | PieSegmentField
+      | TableFields
+    >;
+    animation: FieldAdjuster<BarConfig, AnimationField | undefined>;
+  };
+};
+
 type LineAdjusters = BaseAdjusters<LineConfig> & {
   fields: {
     x: { componentId: FieldAdjuster<LineConfig, string> };
@@ -1081,6 +1168,7 @@ type ComboLineColumnAdjusters = BaseAdjusters<ComboLineColumnConfig> & {
 
 export type ChartConfigsAdjusters = {
   column: ColumnAdjusters;
+  bar: BarAdjusters;
   line: LineAdjusters;
   area: AreaAdjusters;
   scatterplot: ScatterPlotAdjusters;
diff --git a/app/icons/index.tsx b/app/icons/index.tsx
index 1896f9f0d..a54f690a0 100644
--- a/app/icons/index.tsx
+++ b/app/icons/index.tsx
@@ -41,6 +41,8 @@ export const getChartIcon = (chartType: ChartType): IconName => {
       return "chartArea";
     case "column":
       return "chartColumn";
+    case "bar":
+      return "chartBar";
     case "line":
       return "chartLine";
     case "map":

From c85ea4093bd4e119e8eacf4deb2b6322b12e4655 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Fri, 29 Nov 2024 11:24:53 +0000
Subject: [PATCH 03/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-grouped-state-props.ts    |  72 ++---
 app/charts/bar/bars-grouped-state.tsx         | 234 +++++++--------
 app/charts/bar/bars-grouped.tsx               |  74 ++---
 app/charts/bar/bars-stacked-state-props.ts    |  74 ++---
 app/charts/bar/bars-stacked-state.tsx         | 230 +++++++--------
 app/charts/bar/bars-stacked.tsx               |  20 +-
 app/charts/bar/bars-state-props.ts            |  70 ++---
 app/charts/bar/bars-state.tsx                 | 168 +++++------
 app/charts/bar/bars.tsx                       |  74 ++---
 app/charts/bar/chart-bar.tsx                  |  10 +-
 app/charts/bar/overlay-bars.tsx               |  18 +-
 app/charts/bar/rendering-utils.ts             |  12 +-
 app/charts/chart-config-ui-options.ts         |   4 +-
 app/charts/index.ts                           |  10 +-
 .../shared/axis-width-band-vertical.tsx       | 134 +++++++++
 app/charts/shared/axis-width-linear.tsx       |   6 +-
 app/charts/shared/chart-helpers.tsx           | 162 +++++++++++
 app/charts/shared/chart-state.ts              | 273 +++++++++++++++++-
 app/charts/shared/imputation.tsx              |  53 ++++
 .../shared/interaction/tooltip-content.tsx    |  68 +++++
 app/charts/shared/interaction/tooltip.tsx     |  68 ++++-
 app/charts/shared/rendering-utils.ts          | 122 ++++++++
 app/charts/shared/stacked-helpers.ts          |  71 +++++
 app/config-types.ts                           |   4 +-
 24 files changed, 1491 insertions(+), 540 deletions(-)
 create mode 100644 app/charts/shared/axis-width-band-vertical.tsx

diff --git a/app/charts/bar/bars-grouped-state-props.ts b/app/charts/bar/bars-grouped-state-props.ts
index a53dc865a..4877d4fed 100644
--- a/app/charts/bar/bars-grouped-state-props.ts
+++ b/app/charts/bar/bars-grouped-state-props.ts
@@ -4,21 +4,21 @@ import { useCallback, useMemo } from "react";
 
 import { usePlottableData } from "@/charts/shared/chart-helpers";
 import {
-  BandXVariables,
+  BandYVariables,
   BaseVariables,
   ChartStateData,
   InteractiveFiltersVariables,
-  NumericalYErrorVariables,
-  NumericalYVariables,
+  NumericalXErrorVariables,
+  NumericalXVariables,
   RenderingVariables,
   SegmentVariables,
   SortingVariables,
-  useBandXVariables,
+  useBandYVariables,
   useBaseVariables,
   useChartData,
   useInteractiveFiltersVariables,
-  useNumericalYErrorVariables,
-  useNumericalYVariables,
+  useNumericalXErrorVariables,
+  useNumericalXVariables,
   useSegmentVariables,
 } from "@/charts/shared/chart-state";
 import { useRenderingKeyVariable } from "@/charts/shared/rendering-utils";
@@ -30,9 +30,9 @@ import { ChartProps } from "../shared/ChartProps";
 
 export type BarsGroupedStateVariables = BaseVariables &
   SortingVariables &
-  BandXVariables &
-  NumericalYVariables &
-  NumericalYErrorVariables &
+  BandYVariables &
+  NumericalXVariables &
+  NumericalXErrorVariables &
   SegmentVariables &
   RenderingVariables &
   InteractiveFiltersVariables;
@@ -50,19 +50,19 @@ export const useBarsGroupedStateVariables = (
   } = props;
   const { fields, interactiveFiltersConfig } = chartConfig;
   const { x, y, segment, animation } = fields;
-  const xDimension = dimensionsById[x.componentId];
+  const yDimension = dimensionsById[y.componentId];
   const filters = useChartConfigFilters(chartConfig);
 
   const baseVariables = useBaseVariables(chartConfig);
-  const bandXVariables = useBandXVariables(x, {
+  const numericalXVariables = useNumericalXVariables("bar", x, {
+    measuresById,
+  });
+  const bandYVariables = useBandYVariables(y, {
     dimensionsById,
     observations,
   });
-  const numericalYVariables = useNumericalYVariables("bar", y, {
-    measuresById,
-  });
-  const numericalYErrorVariables = useNumericalYErrorVariables(y, {
-    numericalYVariables,
+  const numericalYErrorVariables = useNumericalXErrorVariables(x, {
+    numericalXVariables,
     dimensions,
     measures,
   });
@@ -75,33 +75,33 @@ export const useBarsGroupedStateVariables = (
     { dimensionsById }
   );
 
-  const { getX, getXAsDate } = bandXVariables;
-  const { getY } = numericalYVariables;
+  const { getY, getYAsDate } = bandYVariables;
+  const { getX } = numericalXVariables;
   const sortData: BarsGroupedStateVariables["sortData"] = useCallback(
     (data) => {
-      const { sortingOrder, sortingType } = x.sorting ?? {};
-      const xGetter = isTemporalEntityDimension(xDimension)
-        ? (d: Observation) => getXAsDate(d).getTime().toString()
-        : getX;
+      const { sortingOrder, sortingType } = y.sorting ?? {};
+      const yGetter = isTemporalEntityDimension(yDimension)
+        ? (d: Observation) => getYAsDate(d).getTime().toString()
+        : getY;
       const order = [
         ...rollup(
           data,
-          (v) => sum(v, (d) => getY(d)),
-          (d) => xGetter(d)
+          (v) => sum(v, (d) => getX(d)),
+          (d) => yGetter(d)
         ),
       ]
         .sort((a, b) => ascending(a[1], b[1]))
         .map((d) => d[0]);
 
       if (sortingType === "byDimensionLabel") {
-        return orderBy(data, xGetter, sortingOrder);
+        return orderBy(data, yGetter, sortingOrder);
       } else if (sortingType === "byMeasure") {
-        return sortByIndex({ data, order, getCategory: xGetter, sortingOrder });
+        return sortByIndex({ data, order, getCategory: yGetter, sortingOrder });
       } else {
-        return orderBy(data, xGetter, "asc");
+        return orderBy(data, yGetter, "asc");
       }
     },
-    [getX, getXAsDate, getY, x.sorting, xDimension]
+    [getX, getYAsDate, getY, y.sorting, yDimension]
   );
 
   const getRenderingKey = useRenderingKeyVariable(
@@ -114,8 +114,8 @@ export const useBarsGroupedStateVariables = (
   return {
     ...baseVariables,
     sortData,
-    ...bandXVariables,
-    ...numericalYVariables,
+    ...bandYVariables,
+    ...numericalXVariables,
     ...numericalYErrorVariables,
     ...segmentVariables,
     ...interactiveFiltersVariables,
@@ -130,22 +130,22 @@ export const useBarsGroupedStateData = (
   const { chartConfig, observations } = chartProps;
   const {
     sortData,
-    xDimension,
-    getXAsDate,
-    getY,
+    yDimension,
+    getYAsDate,
+    getX,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   } = variables;
   const plottableData = usePlottableData(observations, {
-    getY,
+    getX,
   });
   const sortedPlottableData = useMemo(() => {
     return sortData(plottableData);
   }, [sortData, plottableData]);
   const data = useChartData(sortedPlottableData, {
     chartConfig,
-    timeRangeDimensionId: xDimension.id,
-    getXAsDate,
+    timeRangeDimensionId: yDimension.id,
+    getXAsDate: getYAsDate,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   });
diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx
index 302346410..71716c822 100644
--- a/app/charts/bar/bars-grouped-state.tsx
+++ b/app/charts/bar/bars-grouped-state.tsx
@@ -31,9 +31,9 @@ import {
   ChartContext,
   ChartStateData,
   CommonChartState,
-  InteractiveXTimeRangeState,
+  InteractiveYTimeRangeState,
 } from "@/charts/shared/chart-state";
-import { TooltipInfo } from "@/charts/shared/interaction/tooltip";
+import { TooltipInfoInverted } from "@/charts/shared/interaction/tooltip";
 import {
   getCenteredTooltipPlacement,
   MOBILE_TOOLTIP_PLACEMENT,
@@ -56,17 +56,17 @@ import { ChartProps } from "../shared/ChartProps";
 
 export type GroupedBarsState = CommonChartState &
   BarsGroupedStateVariables &
-  InteractiveXTimeRangeState & {
+  InteractiveYTimeRangeState & {
     chartType: "bar";
-    xScale: ScaleBand<string>;
-    xScaleInteraction: ScaleBand<string>;
-    xScaleIn: ScaleBand<string>;
-    yScale: ScaleLinear<number, number>;
+    yScale: ScaleBand<string>;
+    yScaleInteraction: ScaleBand<string>;
+    yScaleIn: ScaleBand<string>;
+    xScale: ScaleLinear<number, number>;
     segments: string[];
     colors: ScaleOrdinal<string, string>;
     getColorLabel: (segment: string) => string;
     grouped: [string, Observation[]][];
-    getAnnotationInfo: (d: Observation) => TooltipInfo;
+    getAnnotationInfo: (d: Observation) => TooltipInfoInverted;
   };
 
 const useBarsGroupedState = (
@@ -76,18 +76,18 @@ const useBarsGroupedState = (
 ): GroupedBarsState => {
   const { chartConfig } = chartProps;
   const {
-    xDimension,
+    yDimension,
     getX,
-    getXAsDate,
-    getXAbbreviationOrLabel,
-    getXLabel,
-    yMeasure,
+    getYAsDate,
+    getYAbbreviationOrLabel,
+    getYLabel,
+    xMeasure,
     getY,
-    getMinY,
-    showYStandardError,
-    yErrorMeasure,
-    getYError,
-    getYErrorRange,
+    getMinX,
+    showXStandardError,
+    xErrorMeasure,
+    getXError,
+    getXErrorRange,
     segmentDimension,
     segmentsByAbbreviationOrLabel,
     getSegment,
@@ -121,11 +121,11 @@ const useBarsGroupedState = (
     return Object.fromEntries(
       rollup(
         segmentData,
-        (v) => sum(v, (x) => getY(x)),
-        (x) => getSegment(x)
+        (v) => sum(v, (y) => getX(y)),
+        (y) => getSegment(y)
       )
     );
-  }, [segmentData, getY, getSegment]);
+  }, [segmentData, getX, getSegment]);
 
   const segmentFilter = segmentDimension?.id
     ? chartConfig.cubes.find((d) => d.iri === segmentDimension.cubeIri)
@@ -167,27 +167,27 @@ const useBarsGroupedState = (
   ]);
 
   /* Scales */
-  const xFilter = chartConfig.cubes.find((d) => d.iri === xDimension.cubeIri)
-    ?.filters[xDimension.id];
-  const sumsByX = useMemo(() => {
+  const yFilter = chartConfig.cubes.find((d) => d.iri === yDimension.cubeIri)
+    ?.filters[yDimension.id];
+  const sumsByY = useMemo(() => {
     return Object.fromEntries(
       rollup(
         chartData,
-        (v) => sum(v, (x) => getY(x)),
-        (x) => getX(x)
+        (v) => sum(v, (y) => getX(y)),
+        (y) => getY(y)
       )
     );
   }, [chartData, getX, getY]);
 
   const {
-    xTimeRangeDomainLabels,
+    yTimeRangeDomainLabels,
     colors,
-    yScale,
-    paddingYScale,
-    xScaleTimeRange,
     xScale,
-    xScaleIn,
-    xScaleInteraction,
+    paddingYScale,
+    yScaleTimeRange,
+    yScale,
+    yScaleIn,
+    yScaleInteraction,
   } = useMemo(() => {
     const colors = scaleOrdinal<string, string>();
 
@@ -213,54 +213,54 @@ const useBarsGroupedState = (
 
     colors.unknown(() => undefined);
 
-    const xValues = [...new Set(scalesData.map(getX))];
-    const xTimeRangeValues = [...new Set(timeRangeData.map(getX))];
-    const xSorting = fields.x?.sorting;
-    const xSorters = makeDimensionValueSorters(xDimension, {
-      sorting: xSorting,
-      useAbbreviations: fields.x?.useAbbreviations,
-      measureBySegment: sumsByX,
-      dimensionFilter: xFilter,
+    const yValues = [...new Set(scalesData.map(getY))];
+    const yTimeRangeValues = [...new Set(timeRangeData.map(getY))];
+    const ySorting = fields.y?.sorting;
+    const ySorters = makeDimensionValueSorters(yDimension, {
+      sorting: ySorting,
+      useAbbreviations: fields.y?.useAbbreviations,
+      measureBySegment: sumsByY,
+      dimensionFilter: yFilter,
     });
-    const xDomain = orderBy(
-      xValues,
-      xSorters,
-      getSortingOrders(xSorters, xSorting)
+    const yDomain = orderBy(
+      yValues,
+      ySorters,
+      getSortingOrders(ySorters, ySorting)
     );
-    const xTimeRangeDomainLabels = xTimeRangeValues.map(getXLabel);
-    const xScale = scaleBand()
-      .domain(xDomain)
+    const yTimeRangeDomainLabels = yTimeRangeValues.map(getYLabel);
+    const yScale = scaleBand()
+      .domain(yDomain)
       .paddingInner(PADDING_INNER)
       .paddingOuter(PADDING_OUTER);
-    const xScaleInteraction = scaleBand()
-      .domain(xDomain)
+    const yScaleInteraction = scaleBand()
+      .domain(yDomain)
       .paddingInner(0)
       .paddingOuter(0);
-    const xScaleIn = scaleBand().domain(segments).padding(PADDING_WITHIN);
+    const yScaleIn = scaleBand().domain(segments).padding(PADDING_WITHIN);
 
-    const xScaleTimeRangeDomain = extent(timeRangeData, (d) =>
-      getXAsDate(d)
+    const yScaleTimeRangeDomain = extent(timeRangeData, (d) =>
+      getYAsDate(d)
     ) as [Date, Date];
-    const xScaleTimeRange = scaleTime().domain(xScaleTimeRangeDomain);
+    const yScaleTimeRange = scaleTime().domain(yScaleTimeRangeDomain);
 
-    // y
-    const minValue = getMinY(scalesData, (d) =>
-      getYErrorRange ? getYErrorRange(d)[0] : getY(d)
+    // x
+    const minValue = getMinX(scalesData, (d) =>
+      getXErrorRange ? getXErrorRange(d)[0] : getX(d)
     );
     const maxValue = Math.max(
       max(scalesData, (d) =>
-        getYErrorRange ? getYErrorRange(d)[1] : getY(d)
+        getXErrorRange ? getXErrorRange(d)[1] : getX(d)
       ) ?? 0,
       0
     );
-    const yScale = scaleLinear().domain([minValue, maxValue]).nice();
+    const xScale = scaleLinear().domain([minValue, maxValue]).nice();
 
-    const minPaddingValue = getMinY(paddingData, (d) =>
-      getYErrorRange ? getYErrorRange(d)[0] : getY(d)
+    const minPaddingValue = getMinX(paddingData, (d) =>
+      getXErrorRange ? getXErrorRange(d)[0] : getX(d)
     );
     const maxPaddingValue = Math.max(
       max(paddingData, (d) =>
-        getYErrorRange ? getYErrorRange(d)[1] : getY(d)
+        getXErrorRange ? getXErrorRange(d)[1] : getX(d)
       ) ?? 0,
       0
     );
@@ -270,44 +270,44 @@ const useBarsGroupedState = (
 
     return {
       colors,
-      yScale,
-      paddingYScale,
-      xScaleTimeRange,
       xScale,
-      xScaleIn,
-      xScaleInteraction,
-      xTimeRangeDomainLabels,
+      paddingYScale,
+      yScaleTimeRange,
+      yScale,
+      yScaleIn,
+      yScaleInteraction,
+      yTimeRangeDomainLabels,
     };
   }, [
     fields.segment,
-    fields.x?.sorting,
-    fields.x?.useAbbreviations,
+    fields.y?.sorting,
+    fields.y?.useAbbreviations,
     segmentDimension,
     scalesData,
-    getX,
-    xDimension,
-    sumsByX,
-    xFilter,
-    getXLabel,
+    getY,
+    yDimension,
+    sumsByY,
+    yFilter,
+    getYLabel,
     segments,
     timeRangeData,
     paddingData,
     allSegments,
     segmentsByAbbreviationOrLabel,
     segmentsByValue,
-    getXAsDate,
-    getYErrorRange,
-    getY,
-    getMinY,
+    getYAsDate,
+    getXErrorRange,
+    getX,
+    getMinX,
   ]);
 
   // Group
   const grouped: [string, Observation[]][] = useMemo(() => {
-    const xKeys = xScale.domain();
-    const groupedMap = group(chartData, getX);
+    const yKeys = yScale.domain();
+    const groupedMap = group(chartData, getY);
     const grouped: [string, Observation[]][] =
-      groupedMap.size < xKeys.length
-        ? xKeys.map((d) => {
+      groupedMap.size < yKeys.length
+        ? yKeys.map((d) => {
             if (groupedMap.has(d)) {
               return [d, groupedMap.get(d) as Observation[]];
             } else {
@@ -327,7 +327,7 @@ const useBarsGroupedState = (
         }),
       ];
     });
-  }, [getSegment, getX, chartData, segmentSortingOrder, segments, xScale]);
+  }, [getSegment, getY, chartData, segmentSortingOrder, segments, yScale]);
 
   const { left, bottom } = useChartPadding({
     yScale: paddingYScale,
@@ -336,13 +336,13 @@ const useBarsGroupedState = (
     interactiveFiltersConfig,
     animationPresent: !!fields.animation,
     formatNumber,
-    bandDomain: xTimeRangeDomainLabels.every((d) => d === undefined)
-      ? xScale.domain()
-      : xTimeRangeDomainLabels,
+    bandDomain: yTimeRangeDomainLabels.every((d) => d === undefined)
+      ? yScale.domain()
+      : yTimeRangeDomainLabels,
   });
   const right = 40;
   const { offset: yAxisLabelMargin } = useAxisLabelHeightOffset({
-    label: yMeasure.label,
+    label: xMeasure.label,
     width,
     marginLeft: left,
     marginRight: right,
@@ -357,21 +357,21 @@ const useBarsGroupedState = (
   const { chartWidth, chartHeight } = bounds;
 
   // Adjust of scales based on chart dimensions
-  xScale.range([0, chartWidth]);
-  xScaleInteraction.range([0, chartWidth]);
-  xScaleIn.range([0, xScale.bandwidth()]);
-  xScaleTimeRange.range([0, chartWidth]);
-  yScale.range([chartHeight, 0]);
+  yScale.range([0, chartWidth]);
+  yScaleInteraction.range([0, chartWidth]);
+  yScaleIn.range([0, yScale.bandwidth()]);
+  yScaleTimeRange.range([0, chartWidth]);
+  xScale.range([chartHeight, 0]);
 
   const isMobile = useIsMobile();
 
   // Tooltip
-  const getAnnotationInfo = (datum: Observation): TooltipInfo => {
-    const bw = xScale.bandwidth();
-    const x = getX(datum);
+  const getAnnotationInfo = (datum: Observation): TooltipInfoInverted => {
+    const bw = yScale.bandwidth();
+    const y = getY(datum);
 
-    const tooltipValues = chartData.filter((d) => getX(d) === x);
-    const yValues = tooltipValues.map(getY);
+    const tooltipValues = chartData.filter((d) => getY(d) === y);
+    const xValues = tooltipValues.map(getX);
     const sortedTooltipValues = sortByIndex({
       data: tooltipValues,
       order: segments,
@@ -379,49 +379,49 @@ const useBarsGroupedState = (
       // Always ascending to match visual order of colors of the stack
       sortingOrder: "asc",
     });
-    const yValueFormatter = (value: number | null) => {
+    const xValueFormatter = (value: number | null) => {
       return formatNumberWithUnit(
         value,
-        formatters[yMeasure.id] ?? formatNumber,
-        yMeasure.unit
+        formatters[xMeasure.id] ?? formatNumber,
+        xMeasure.unit
       );
     };
 
-    const xAnchorRaw = (xScale(x) as number) + bw * 0.5;
-    const [yMin, yMax] = extent(yValues, (d) => d ?? 0) as [number, number];
-    const yAnchor = isMobile ? chartHeight : yScale((yMin + yMax) * 0.5);
+    const yAnchorRaw = (yScale(y) as number) + bw * 0.5;
+    const [xMin, xMax] = extent(xValues, (d) => d ?? 0) as [number, number];
+    const xAnchor = isMobile ? chartHeight : xScale((xMin + xMax) * 0.5);
     const placement = isMobile
       ? MOBILE_TOOLTIP_PLACEMENT
       : getCenteredTooltipPlacement({
           chartWidth,
-          xAnchor: xAnchorRaw,
+          xAnchor: yAnchorRaw,
           topAnchor: !fields.segment,
         });
 
     const getError = (d: Observation) => {
-      if (!showYStandardError || !getYError || getYError(d) == null) {
+      if (!showXStandardError || !getXError || getXError(d) == null) {
         return;
       }
 
-      return `${getYError(d)}${yErrorMeasure?.unit ?? ""}`;
+      return `${getXError(d)}${xErrorMeasure?.unit ?? ""}`;
     };
 
     return {
-      xAnchor: xAnchorRaw + (placement.x === "right" ? 0.5 : -0.5) * bw,
-      yAnchor,
+      yAnchor: yAnchorRaw + (placement.y === "bottom" ? 0.5 : -0.5) * bw,
+      xAnchor,
       placement,
-      xValue: getXAbbreviationOrLabel(datum),
+      yValue: getYAbbreviationOrLabel(datum),
       datum: {
         label: fields.segment && getSegmentAbbreviationOrLabel(datum),
-        value: yValueFormatter(getY(datum)),
+        value: xValueFormatter(getX(datum)),
         error: getError(datum),
         color: colors(getSegment(datum)) as string,
       },
       values: sortedTooltipValues.map((td) => ({
         label: getSegmentAbbreviationOrLabel(td),
-        value: yMeasure.unit
-          ? `${formatNumber(getY(td))} ${yMeasure.unit}`
-          : formatNumber(getY(td)),
+        value: xMeasure.unit
+          ? `${formatNumber(getX(td))} ${xMeasure.unit}`
+          : formatNumber(getX(td)),
         error: getError(td),
         color: colors(getSegment(td)) as string,
       })),
@@ -433,11 +433,11 @@ const useBarsGroupedState = (
     bounds,
     chartData,
     allData,
-    xScale,
-    xScaleInteraction,
-    xScaleIn,
-    xScaleTimeRange,
     yScale,
+    yScaleInteraction,
+    yScaleIn,
+    yScaleTimeRange,
+    xScale,
     segments,
     colors,
     getColorLabel: getSegmentLabel,
diff --git a/app/charts/bar/bars-grouped.tsx b/app/charts/bar/bars-grouped.tsx
index c293fd57d..3b9d9c18a 100644
--- a/app/charts/bar/bars-grouped.tsx
+++ b/app/charts/bar/bars-grouped.tsx
@@ -4,10 +4,10 @@ import { GroupedBarsState } from "@/charts/bar/bars-grouped-state";
 import { RenderBarDatum, renderBars } from "@/charts/bar/rendering-utils";
 import { useChartState } from "@/charts/shared/chart-state";
 import {
-  RenderWhiskerDatum,
   filterWithoutErrors,
+  renderBarWhiskers,
   renderContainer,
-  renderWhiskers,
+  RenderWhiskerBarDatum,
 } from "@/charts/shared/rendering-utils";
 import { useTransitionStore } from "@/stores/transition";
 
@@ -15,49 +15,49 @@ export const ErrorWhiskers = () => {
   const {
     bounds,
     xScale,
-    xScaleIn,
-    getYErrorRange,
-    getYError,
+    yScaleIn,
+    getXErrorRange,
+    getXError,
     yScale,
     getSegment,
     grouped,
-    showYStandardError,
+    showXStandardError,
   } = useChartState() as GroupedBarsState;
   const { margins, width, height } = bounds;
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
-  const renderData: RenderWhiskerDatum[] = useMemo(() => {
-    if (!getYErrorRange || !showYStandardError) {
+  const renderData: RenderWhiskerBarDatum[] = useMemo(() => {
+    if (!getXErrorRange || !showXStandardError) {
       return [];
     }
 
-    const bandwidth = xScaleIn.bandwidth();
+    const bandwidth = yScaleIn.bandwidth();
     return grouped
-      .filter((d) => d[1].some(filterWithoutErrors(getYError)))
+      .filter((d) => d[1].some(filterWithoutErrors(getXError)))
       .flatMap(([segment, observations]) =>
         observations.map((d) => {
-          const x0 = xScaleIn(getSegment(d)) as number;
+          const y0 = yScaleIn(getSegment(d)) as number;
           const barWidth = Math.min(bandwidth, 15);
-          const [y1, y2] = getYErrorRange(d);
+          const [x1, x2] = getXErrorRange(d);
           return {
             key: `${segment}-${getSegment(d)}`,
-            x: (xScale(segment) as number) + x0 + bandwidth / 2 - barWidth / 2,
-            y1: yScale(y1),
-            y2: yScale(y2),
+            y: (yScale(segment) as number) + y0 + bandwidth / 2 - barWidth / 2,
+            x1: xScale(x1),
+            x2: xScale(x2),
             width: barWidth,
-          } as RenderWhiskerDatum;
+          } as RenderWhiskerBarDatum;
         })
       );
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [
     getSegment,
-    getYErrorRange,
-    getYError,
+    getXErrorRange,
+    getXError,
     grouped,
-    showYStandardError,
+    showXStandardError,
     xScale,
-    xScaleIn,
+    yScaleIn,
     yScale,
     width,
     height,
@@ -69,7 +69,7 @@ export const ErrorWhiskers = () => {
         id: "bars-grouped-error-whiskers",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g, opts) => renderWhiskers(g, renderData, opts),
+        render: (g, opts) => renderBarWhiskers(g, renderData, opts),
       });
     }
   }, [
@@ -87,8 +87,8 @@ export const BarsGrouped = () => {
   const {
     bounds,
     xScale,
-    xScaleIn,
-    getY,
+    yScaleIn,
+    getX,
     yScale,
     getSegment,
     colors,
@@ -99,22 +99,22 @@ export const BarsGrouped = () => {
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
   const { margins, height } = bounds;
-  const bandwidth = xScaleIn.bandwidth();
-  const y0 = yScale(0);
+  const bandwidth = yScaleIn.bandwidth();
+  const x0 = xScale(0);
   const renderData: RenderBarDatum[] = useMemo(() => {
     return grouped.flatMap(([segment, observations]) => {
       return observations.map((d) => {
         const key = getRenderingKey(d, getSegment(d));
-        const x = getSegment(d);
-        const y = getY(d) ?? NaN;
+        const y = getSegment(d);
+        const x = getX(d) ?? NaN;
 
         return {
           key,
-          x: (xScale(segment) as number) + (xScaleIn(x) as number),
-          y: yScale(Math.max(y, 0)),
-          width: bandwidth,
-          height: Math.max(0, Math.abs(yScale(y) - y0)),
-          color: colors(x),
+          y: (yScale(segment) as number) + (yScaleIn(y) as number),
+          x: xScale(Math.max(x, 0)),
+          width: Math.max(0, Math.abs(xScale(x) - x0)),
+          height: bandwidth,
+          color: colors(y),
         };
       });
     });
@@ -123,12 +123,12 @@ export const BarsGrouped = () => {
     colors,
     getSegment,
     bandwidth,
-    getY,
+    getX,
     grouped,
-    xScaleIn,
+    yScaleIn,
     xScale,
     yScale,
-    y0,
+    x0,
     getRenderingKey,
     height,
   ]);
@@ -139,7 +139,7 @@ export const BarsGrouped = () => {
         id: "bars-grouped",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g, opts) => renderBars(g, renderData, { ...opts, y0 }),
+        render: (g, opts) => renderBars(g, renderData, { ...opts, x0 }),
       });
     }
   }, [
@@ -148,7 +148,7 @@ export const BarsGrouped = () => {
     margins.top,
     renderData,
     transitionDuration,
-    y0,
+    x0,
   ]);
 
   return <g ref={ref} />;
diff --git a/app/charts/bar/bars-stacked-state-props.ts b/app/charts/bar/bars-stacked-state-props.ts
index f8584cc91..eb0f105ea 100644
--- a/app/charts/bar/bars-stacked-state-props.ts
+++ b/app/charts/bar/bars-stacked-state-props.ts
@@ -3,19 +3,19 @@ import { useCallback, useMemo } from "react";
 
 import { getWideData, usePlottableData } from "@/charts/shared/chart-helpers";
 import {
-  BandXVariables,
+  BandYVariables,
   BaseVariables,
   ChartStateData,
   InteractiveFiltersVariables,
-  NumericalYVariables,
+  NumericalXVariables,
   RenderingVariables,
   SegmentVariables,
   SortingVariables,
-  useBandXVariables,
+  useBandYVariables,
   useBaseVariables,
   useChartData,
   useInteractiveFiltersVariables,
-  useNumericalYVariables,
+  useNumericalXVariables,
   useSegmentVariables,
 } from "@/charts/shared/chart-state";
 import { useRenderingKeyVariable } from "@/charts/shared/rendering-utils";
@@ -27,8 +27,8 @@ import { ChartProps } from "../shared/ChartProps";
 
 export type BarsStackedStateVariables = BaseVariables &
   SortingVariables<{ plottableDataWide: Observation[] }> &
-  BandXVariables &
-  NumericalYVariables &
+  BandYVariables &
+  NumericalXVariables &
   SegmentVariables &
   RenderingVariables &
   InteractiveFiltersVariables;
@@ -45,17 +45,17 @@ export const useBarsStackedStateVariables = (
   } = props;
   const { fields, interactiveFiltersConfig } = chartConfig;
   const { x, y, segment, animation } = fields;
-  const xDimension = dimensionsById[x.componentId];
+  const yDimension = dimensionsById[y.componentId];
   const filters = useChartConfigFilters(chartConfig);
 
   const baseVariables = useBaseVariables(chartConfig);
-  const bandXVariables = useBandXVariables(x, {
+  const numericalXVariables = useNumericalXVariables("bar", x, {
+    measuresById,
+  });
+  const bandYVariables = useBandYVariables(y, {
     dimensionsById,
     observations,
   });
-  const numericalYVariables = useNumericalYVariables("bar", y, {
-    measuresById,
-  });
   const segmentVariables = useSegmentVariables(segment, {
     dimensionsById,
     observations,
@@ -65,33 +65,33 @@ export const useBarsStackedStateVariables = (
     { dimensionsById }
   );
 
-  const { getX, getXAsDate } = bandXVariables;
+  const { getY, getYAsDate } = bandYVariables;
   const sortData: BarsStackedStateVariables["sortData"] = useCallback(
     (data, { plottableDataWide }) => {
-      const { sortingOrder, sortingType } = x.sorting ?? {};
-      const xGetter = isTemporalEntityDimension(xDimension)
-        ? (d: Observation) => getXAsDate(d).getTime().toString()
-        : getX;
-      const xOrder = plottableDataWide
+      const { sortingOrder, sortingType } = y.sorting ?? {};
+      const yGetter = isTemporalEntityDimension(yDimension)
+        ? (d: Observation) => getYAsDate(d).getTime().toString()
+        : getY;
+      const yOrder = plottableDataWide
         .sort((a, b) => ascending(a.total ?? undefined, b.total ?? undefined))
-        .map(xGetter);
+        .map(yGetter);
 
       if (sortingOrder === "desc" && sortingType === "byDimensionLabel") {
-        return [...data].sort((a, b) => descending(xGetter(a), xGetter(b)));
+        return [...data].sort((a, b) => descending(yGetter(a), yGetter(b)));
       } else if (sortingOrder === "asc" && sortingType === "byDimensionLabel") {
-        return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b)));
+        return [...data].sort((a, b) => ascending(yGetter(a), yGetter(b)));
       } else if (sortingType === "byMeasure") {
         return sortByIndex({
           data,
-          order: xOrder,
-          getCategory: xGetter,
+          order: yOrder,
+          getCategory: yGetter,
           sortingOrder,
         });
       } else {
-        return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b)));
+        return [...data].sort((a, b) => ascending(yGetter(a), yGetter(b)));
       }
     },
-    [getX, getXAsDate, x.sorting, xDimension]
+    [getY, getYAsDate, y.sorting, yDimension]
   );
 
   const getRenderingKey = useRenderingKeyVariable(
@@ -104,8 +104,8 @@ export const useBarsStackedStateVariables = (
   return {
     ...baseVariables,
     sortData,
-    ...bandXVariables,
-    ...numericalYVariables,
+    ...bandYVariables,
+    ...numericalXVariables,
     ...segmentVariables,
     ...interactiveFiltersVariables,
     getRenderingKey,
@@ -122,26 +122,26 @@ export const useBarsStackedStateData = (
 ): BarsStackedStateData => {
   const { chartConfig, observations } = chartProps;
   const { fields } = chartConfig;
-  const { x } = fields;
+  const { y } = fields;
   const {
     sortData,
-    xDimension,
+    yDimension,
     getX,
-    getXAsDate,
+    getYAsDate,
     getY,
     getSegment,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   } = variables;
   const plottableData = usePlottableData(observations, {
-    getY,
+    getX,
   });
   const { sortedPlottableData, plottableDataWide } = useMemo(() => {
-    const plottableDataByX = group(plottableData, getX);
+    const plottableDataByY = group(plottableData, getY);
     const plottableDataWide = getWideData({
-      dataGroupedByX: plottableDataByX,
-      xKey: x.componentId,
-      getY,
+      dataGroupedByX: plottableDataByY,
+      xKey: y.componentId,
+      getY: getX,
       getSegment,
     });
 
@@ -151,11 +151,11 @@ export const useBarsStackedStateData = (
       }),
       plottableDataWide,
     };
-  }, [plottableData, getX, x.componentId, getY, getSegment, sortData]);
+  }, [plottableData, getX, y.componentId, getY, getSegment, sortData]);
   const data = useChartData(sortedPlottableData, {
     chartConfig,
-    timeRangeDimensionId: xDimension.id,
-    getXAsDate,
+    timeRangeDimensionId: yDimension.id,
+    getXAsDate: getYAsDate,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   });
diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index d6d58ce24..76f892174 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -32,23 +32,23 @@ import {
   useChartPadding,
 } from "@/charts/shared/chart-dimensions";
 import {
-  getWideData,
-  normalizeData,
-  useGetIdentityY,
+  getWideDataInverted,
+  normalizeDataInverted,
+  useGetIdentityX,
 } from "@/charts/shared/chart-helpers";
 import {
   ChartContext,
   CommonChartState,
-  InteractiveXTimeRangeState,
+  InteractiveYTimeRangeState,
 } from "@/charts/shared/chart-state";
-import { TooltipInfo } from "@/charts/shared/interaction/tooltip";
+import { TooltipInfoInverted } from "@/charts/shared/interaction/tooltip";
 import {
   getCenteredTooltipPlacement,
   MOBILE_TOOLTIP_PLACEMENT,
 } from "@/charts/shared/interaction/tooltip-box";
 import {
-  getStackedTooltipValueFormatter,
-  getStackedYScale,
+  getStackedTooltipValueFormatterInverted,
+  getStackedXScale,
 } from "@/charts/shared/stacked-helpers";
 import useChartFormatters from "@/charts/shared/use-chart-formatters";
 import { InteractionProvider } from "@/charts/shared/use-interaction";
@@ -69,11 +69,11 @@ import { ChartProps } from "../shared/ChartProps";
 
 export type StackedBarsState = CommonChartState &
   BarsStackedStateVariables &
-  InteractiveXTimeRangeState & {
+  InteractiveYTimeRangeState & {
     chartType: "bar";
-    xScale: ScaleBand<string>;
-    xScaleInteraction: ScaleBand<string>;
-    yScale: ScaleLinear<number, number>;
+    yScale: ScaleBand<string>;
+    yScaleInteraction: ScaleBand<string>;
+    xScale: ScaleLinear<number, number>;
     segments: string[];
     colors: ScaleOrdinal<string, string>;
     getColorLabel: (segment: string) => string;
@@ -82,7 +82,7 @@ export type StackedBarsState = CommonChartState &
     getAnnotationInfo: (
       d: Observation,
       orderedSegments: string[]
-    ) => TooltipInfo;
+    ) => TooltipInfoInverted;
   };
 
 const useBarsStackedState = (
@@ -92,12 +92,12 @@ const useBarsStackedState = (
 ): StackedBarsState => {
   const { chartConfig } = chartProps;
   const {
-    xDimension,
+    yDimension,
     getX,
-    getXAsDate,
-    getXAbbreviationOrLabel,
-    getXLabel,
-    yMeasure,
+    getYAsDate,
+    getYAbbreviationOrLabel,
+    getYLabel,
+    xMeasure,
     getY,
     segmentDimension,
     segmentsByAbbreviationOrLabel,
@@ -105,7 +105,7 @@ const useBarsStackedState = (
     getSegmentAbbreviationOrLabel,
     getSegmentLabel,
   } = variables;
-  const getIdentityY = useGetIdentityY(yMeasure.id);
+  const getIdentityX = useGetIdentityX(xMeasure.id);
   const {
     chartData,
     scalesData,
@@ -121,7 +121,7 @@ const useBarsStackedState = (
   const formatters = useChartFormatters(chartProps);
   const calculationType = useChartInteractiveFilters((d) => d.calculation.type);
 
-  const xKey = fields.x.componentId;
+  const yKey = fields.y.componentId;
 
   const segmentsByValue = useMemo(() => {
     const values = segmentDimension?.values || [];
@@ -133,11 +133,11 @@ const useBarsStackedState = (
     return Object.fromEntries(
       rollup(
         scalesData,
-        (v) => sum(v, (x) => getY(x)),
+        (v) => sum(v, (x) => getX(x)),
         (x) => getSegment(x)
       )
     );
-  }, [getSegment, getY, scalesData]);
+  }, [getSegment, getX, scalesData]);
 
   const segmentFilter = segmentDimension?.id
     ? chartConfig.cubes.find((d) => d.iri === segmentDimension.cubeIri)
@@ -174,53 +174,53 @@ const useBarsStackedState = (
     getSegment,
   ]);
 
-  const sumsByX = useMemo(() => {
+  const sumsByY = useMemo(() => {
     return Object.fromEntries(
       rollup(
         chartData,
-        (v) => sum(v, (x) => getY(x)),
-        (x) => getX(x)
+        (v) => sum(v, (x) => getX(x)),
+        (x) => getY(x)
       )
     );
   }, [chartData, getX, getY]);
 
   const normalize = calculationType === "percent";
-  const chartDataGroupedByX = useMemo(() => {
+  const chartDataGroupedByY = useMemo(() => {
     if (normalize) {
       return group(
-        normalizeData(chartData, {
-          yKey: yMeasure.id,
-          getY,
-          getTotalGroupValue: (d) => sumsByX[getX(d)],
+        normalizeDataInverted(chartData, {
+          xKey: xMeasure.id,
+          getX,
+          getTotalGroupValue: (d) => sumsByY[getY(d)],
         }),
-        getX
+        getY
       );
     }
 
-    return group(chartData, getX);
-  }, [chartData, getX, sumsByX, getY, yMeasure.id, normalize]);
+    return group(chartData, getY);
+  }, [chartData, getX, sumsByY, getY, xMeasure.id, normalize]);
 
   const chartWideData = useMemo(() => {
-    return getWideData({
-      dataGroupedByX: chartDataGroupedByX,
-      xKey,
-      getY,
+    return getWideDataInverted({
+      dataGroupedByY: chartDataGroupedByY,
+      yKey,
+      getX,
       getSegment,
       allSegments: segments,
       imputationType: "zeros",
     });
-  }, [getSegment, getY, chartDataGroupedByX, segments, xKey]);
+  }, [getSegment, getX, chartDataGroupedByY, segments, yKey]);
 
-  const xFilter = chartConfig.cubes.find((d) => d.iri === xDimension.cubeIri)
-    ?.filters[xDimension.id];
+  const yFilter = chartConfig.cubes.find((d) => d.iri === yDimension.cubeIri)
+    ?.filters[yDimension.id];
 
   // Map ordered segments labels to colors
   const {
     colors,
-    xScale,
-    xTimeRangeDomainLabels,
-    xScaleInteraction,
-    xScaleTimeRange,
+    yScale,
+    yTimeRangeDomainLabels,
+    yScaleInteraction,
+    yScaleTimeRange,
   } = useMemo(() => {
     const colors = scaleOrdinal<string, string>();
 
@@ -257,52 +257,52 @@ const useBarsStackedState = (
 
     colors.unknown(() => undefined);
 
-    const xValues = [...new Set(scalesData.map(getX))];
-    const xTimeRangeValues = [...new Set(timeRangeData.map(getX))];
-    const xSorting = fields.x?.sorting;
-    const xSorters = makeDimensionValueSorters(xDimension, {
-      sorting: xSorting,
-      useAbbreviations: fields.x?.useAbbreviations,
-      measureBySegment: sumsByX,
-      dimensionFilter: xFilter,
+    const yValues = [...new Set(scalesData.map(getY))];
+    const yTimeRangeValues = [...new Set(timeRangeData.map(getY))];
+    const ySorting = fields.y?.sorting;
+    const ySorters = makeDimensionValueSorters(yDimension, {
+      sorting: ySorting,
+      useAbbreviations: fields.y?.useAbbreviations,
+      measureBySegment: sumsByY,
+      dimensionFilter: yFilter,
     });
-    const xDomain = orderBy(
-      xValues,
-      xSorters,
-      getSortingOrders(xSorters, xSorting)
+    const yDomain = orderBy(
+      yValues,
+      ySorters,
+      getSortingOrders(ySorters, ySorting)
     );
-    const xTimeRangeDomainLabels = xTimeRangeValues.map(getXLabel);
-    const xScale = scaleBand()
-      .domain(xDomain)
+    const yTimeRangeDomainLabels = yTimeRangeValues.map(getYLabel);
+    const yScale = scaleBand()
+      .domain(yDomain)
       .paddingInner(PADDING_INNER)
       .paddingOuter(PADDING_OUTER);
-    const xScaleInteraction = scaleBand()
-      .domain(xDomain)
+    const yScaleInteraction = scaleBand()
+      .domain(yDomain)
       .paddingInner(0)
       .paddingOuter(0);
 
-    const xScaleTimeRangeDomain = extent(timeRangeData, (d) =>
-      getXAsDate(d)
+    const yScaleTimeRangeDomain = extent(timeRangeData, (d) =>
+      getYAsDate(d)
     ) as [Date, Date];
-    const xScaleTimeRange = scaleTime().domain(xScaleTimeRangeDomain);
+    const yScaleTimeRange = scaleTime().domain(yScaleTimeRangeDomain);
 
     return {
       colors,
-      xScale,
-      xTimeRangeDomainLabels,
-      xScaleTimeRange,
-      xScaleInteraction,
+      yScale,
+      yTimeRangeDomainLabels,
+      yScaleTimeRange,
+      yScaleInteraction,
     };
   }, [
     fields.segment,
-    fields.x.sorting,
-    fields.x.useAbbreviations,
-    xDimension,
-    xFilter,
-    sumsByX,
-    getX,
-    getXLabel,
-    getXAsDate,
+    fields.y.sorting,
+    fields.y.useAbbreviations,
+    yDimension,
+    yFilter,
+    sumsByY,
+    getY,
+    getYLabel,
+    getYAsDate,
     scalesData,
     timeRangeData,
     segmentsByAbbreviationOrLabel,
@@ -318,21 +318,21 @@ const useBarsStackedState = (
     [animationIri]
   );
 
-  const yScale = useMemo(() => {
-    return getStackedYScale(scalesData, {
+  const xScale = useMemo(() => {
+    return getStackedXScale(scalesData, {
       normalize,
-      getX,
       getY,
+      getX,
       getTime: getAnimation,
     });
   }, [scalesData, normalize, getX, getY, getAnimation]);
 
-  const paddingYScale = useMemo(() => {
+  const paddingXScale = useMemo(() => {
     //  When the user can toggle between absolute and relative values, we use the
     // absolute values to calculate the yScale domain, so that the yScale doesn't
     // change when the user toggles between absolute and relative values.
     if (interactiveFiltersConfig?.calculation.active) {
-      const scale = getStackedYScale(paddingData, {
+      const scale = getStackedXScale(paddingData, {
         normalize: false,
         getX,
         getY,
@@ -346,7 +346,7 @@ const useBarsStackedState = (
       return scale;
     }
 
-    return getStackedYScale(paddingData, {
+    return getStackedXScale(paddingData, {
       normalize,
       getX,
       getY,
@@ -388,20 +388,21 @@ const useBarsStackedState = (
 
   /** Chart dimensions */
   const { left, bottom } = useChartPadding({
-    yScale: paddingYScale,
+    //TODO: This is wrong, need to fix
+    yScale: paddingXScale,
     width,
     height,
     interactiveFiltersConfig,
     animationPresent: !!fields.animation,
     formatNumber,
-    bandDomain: xTimeRangeDomainLabels.every((d) => d === undefined)
-      ? xScale.domain()
-      : xTimeRangeDomainLabels,
+    bandDomain: yTimeRangeDomainLabels.every((d) => d === undefined)
+      ? yScale.domain()
+      : yTimeRangeDomainLabels,
     normalize,
   });
   const right = 40;
   const { offset: yAxisLabelMargin } = useAxisLabelHeightOffset({
-    label: yMeasure.label,
+    label: xMeasure.label,
     width,
     marginLeft: left,
     marginRight: right,
@@ -415,60 +416,61 @@ const useBarsStackedState = (
   const bounds = useChartBounds(width, margins, height);
   const { chartWidth, chartHeight } = bounds;
 
-  xScale.range([0, chartWidth]);
-  xScaleInteraction.range([0, chartWidth]);
-  xScaleTimeRange.range([0, chartWidth]);
-  yScale.range([chartHeight, 0]);
+  yScale.range([0, chartWidth]);
+  yScaleInteraction.range([0, chartWidth]);
+  yScaleTimeRange.range([0, chartWidth]);
+  xScale.range([chartHeight, 0]);
 
   const isMobile = useIsMobile();
 
   // Tooltips
   const getAnnotationInfo = useCallback(
-    (datum: Observation): TooltipInfo => {
-      const bw = xScale.bandwidth();
-      const x = getX(datum);
+    (datum: Observation): TooltipInfoInverted => {
+      const bw = yScale.bandwidth();
+      const y = getY(datum);
 
-      const tooltipValues = chartDataGroupedByX.get(x) as Observation[];
-      const yValues = tooltipValues.map(getY);
+      const tooltipValues = chartDataGroupedByY.get(y) as Observation[];
+      const xValues = tooltipValues.map(getX);
       const sortedTooltipValues = sortByIndex({
         data: tooltipValues,
         order: segments,
         getCategory: getSegment,
         sortingOrder: "asc",
       });
-      const yValueFormatter = getStackedTooltipValueFormatter({
+      const xValueFormatter = getStackedTooltipValueFormatterInverted({
         normalize,
-        yMeasureId: yMeasure.id,
-        yMeasureUnit: yMeasure.unit,
+        xMeasureId: xMeasure.id,
+        xMeasureUnit: xMeasure.unit,
         formatters,
         formatNumber,
       });
 
-      const xAnchorRaw = (xScale(x) as number) + bw * 0.5;
-      const yAnchor = isMobile
+      const yAnchorRaw = (yScale(y) as number) + bw * 0.5;
+      const xAnchor = isMobile
         ? chartHeight
-        : yScale(sum(yValues.map((d) => d ?? 0)) * 0.5);
+        : xScale(sum(xValues.map((d) => d ?? 0)) * 0.5);
       const placement = isMobile
         ? MOBILE_TOOLTIP_PLACEMENT
         : getCenteredTooltipPlacement({
             chartWidth,
-            xAnchor: xAnchorRaw,
+            //NOTE: this might be wrong
+            xAnchor,
             topAnchor: !fields.segment,
           });
 
       return {
-        xAnchor: xAnchorRaw + (placement.x === "right" ? 0.5 : -0.5) * bw,
-        yAnchor,
+        yAnchor: yAnchorRaw + (placement.y === "top" ? 0.5 : -0.5) * bw,
+        xAnchor,
         placement,
-        xValue: getXAbbreviationOrLabel(datum),
+        yValue: getYAbbreviationOrLabel(datum),
         datum: {
           label: fields.segment && getSegmentAbbreviationOrLabel(datum),
-          value: yValueFormatter(getY(datum), getIdentityY(datum)),
+          value: xValueFormatter(getX(datum), getIdentityX(datum)),
           color: colors(getSegment(datum)) as string,
         },
         values: sortedTooltipValues.map((td) => ({
           label: getSegmentAbbreviationOrLabel(td),
-          value: yValueFormatter(getY(td), getIdentityY(td)),
+          value: xValueFormatter(getX(td), getIdentityX(td)),
           color: colors(getSegment(td)) as string,
         })),
       };
@@ -476,18 +478,18 @@ const useBarsStackedState = (
     [
       getX,
       xScale,
-      chartDataGroupedByX,
+      chartDataGroupedByY,
       segments,
       getSegment,
-      yMeasure.id,
-      yMeasure.unit,
+      xMeasure.id,
+      xMeasure.unit,
       formatters,
       formatNumber,
-      getXAbbreviationOrLabel,
+      getYAbbreviationOrLabel,
       fields.segment,
       getSegmentAbbreviationOrLabel,
       getY,
-      getIdentityY,
+      getIdentityX,
       colors,
       chartWidth,
       chartHeight,
@@ -503,8 +505,8 @@ const useBarsStackedState = (
     chartData,
     allData,
     xScale,
-    xScaleInteraction,
-    xScaleTimeRange,
+    yScaleInteraction,
+    yScaleTimeRange,
     yScale,
     segments,
     colors,
diff --git a/app/charts/bar/bars-stacked.tsx b/app/charts/bar/bars-stacked.tsx
index d7d5382c4..72a5bb3ae 100644
--- a/app/charts/bar/bars-stacked.tsx
+++ b/app/charts/bar/bars-stacked.tsx
@@ -10,11 +10,11 @@ export const BarsStacked = () => {
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
-  const { bounds, getX, xScale, yScale, colors, series, getRenderingKey } =
+  const { bounds, getY, xScale, yScale, colors, series, getRenderingKey } =
     useChartState() as StackedBarsState;
   const { margins, height } = bounds;
-  const bandwidth = xScale.bandwidth();
-  const y0 = yScale(0);
+  const bandwidth = yScale.bandwidth();
+  const x0 = xScale(0);
   const renderData: RenderBarDatum[] = useMemo(() => {
     return series.flatMap((d) => {
       const color = colors(d.key);
@@ -24,10 +24,10 @@ export const BarsStacked = () => {
 
         return {
           key: getRenderingKey(observation, d.key),
-          x: xScale(getX(observation)) as number,
-          y: yScale(segment[1]),
-          width: bandwidth,
-          height: Math.max(0, yScale(segment[0]) - yScale(segment[1])),
+          y: yScale(getY(observation)) as number,
+          x: xScale(segment[1]),
+          height: bandwidth,
+          width: Math.max(0, xScale(segment[0]) - xScale(segment[1])),
           color,
         };
       });
@@ -36,7 +36,7 @@ export const BarsStacked = () => {
   }, [
     bandwidth,
     colors,
-    getX,
+    getY,
     series,
     xScale,
     yScale,
@@ -51,7 +51,7 @@ export const BarsStacked = () => {
         id: "bars-stacked",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g, opts) => renderBars(g, renderData, { ...opts, y0 }),
+        render: (g, opts) => renderBars(g, renderData, { ...opts, x0 }),
       });
     }
   }, [
@@ -60,7 +60,7 @@ export const BarsStacked = () => {
     margins.top,
     renderData,
     transitionDuration,
-    y0,
+    x0,
   ]);
 
   return <g ref={ref} />;
diff --git a/app/charts/bar/bars-state-props.ts b/app/charts/bar/bars-state-props.ts
index 5859052b9..c9fb5344a 100644
--- a/app/charts/bar/bars-state-props.ts
+++ b/app/charts/bar/bars-state-props.ts
@@ -3,20 +3,20 @@ import { useCallback, useMemo } from "react";
 
 import { usePlottableData } from "@/charts/shared/chart-helpers";
 import {
-  BandXVariables,
+  BandYVariables,
   BaseVariables,
   ChartStateData,
   InteractiveFiltersVariables,
-  NumericalYErrorVariables,
-  NumericalYVariables,
+  NumericalXErrorVariables,
+  NumericalXVariables,
   RenderingVariables,
   SortingVariables,
-  useBandXVariables,
+  useBandYVariables,
+  useBarChartData,
   useBaseVariables,
-  useChartData,
   useInteractiveFiltersVariables,
-  useNumericalYErrorVariables,
-  useNumericalYVariables,
+  useNumericalXErrorVariables,
+  useNumericalXVariables,
 } from "@/charts/shared/chart-state";
 import { useRenderingKeyVariable } from "@/charts/shared/rendering-utils";
 import { BarConfig, useChartConfigFilters } from "@/configurator";
@@ -26,9 +26,9 @@ import { ChartProps } from "../shared/ChartProps";
 
 export type BarsStateVariables = BaseVariables &
   SortingVariables &
-  BandXVariables &
-  NumericalYVariables &
-  NumericalYErrorVariables &
+  NumericalXVariables &
+  BandYVariables &
+  NumericalXErrorVariables &
   RenderingVariables &
   InteractiveFiltersVariables;
 
@@ -45,19 +45,19 @@ export const useBarsStateVariables = (
   } = props;
   const { fields, interactiveFiltersConfig } = chartConfig;
   const { x, y, animation } = fields;
-  const xDimension = dimensionsById[x.componentId];
+  const yDimension = dimensionsById[y.componentId];
   const filters = useChartConfigFilters(chartConfig);
 
   const baseVariables = useBaseVariables(chartConfig);
-  const bandXVariables = useBandXVariables(x, {
+  const numericalXVariables = useNumericalXVariables("bar", x, {
+    measuresById,
+  });
+  const bandYVariables = useBandYVariables(y, {
     dimensionsById,
     observations,
   });
-  const numericalYVariables = useNumericalYVariables("bar", y, {
-    measuresById,
-  });
-  const numericalYErrorVariables = useNumericalYErrorVariables(y, {
-    numericalYVariables,
+  const numericalXErrorVariables = useNumericalXErrorVariables(x, {
+    numericalXVariables,
     dimensions,
     measures,
   });
@@ -66,29 +66,29 @@ export const useBarsStateVariables = (
     { dimensionsById }
   );
 
-  const { getX, getXAsDate } = bandXVariables;
-  const { getY } = numericalYVariables;
+  const { getY, getYAsDate } = bandYVariables;
+  const { getX } = numericalXVariables;
   const sortData: BarsStateVariables["sortData"] = useCallback(
     (data) => {
-      const { sortingOrder, sortingType } = x.sorting ?? {};
-      const xGetter = isTemporalEntityDimension(xDimension) ? getXAsDate : getX;
+      const { sortingOrder, sortingType } = y.sorting ?? {};
+      const yGetter = isTemporalEntityDimension(yDimension) ? getYAsDate : getY;
       if (sortingOrder === "desc" && sortingType === "byDimensionLabel") {
-        return [...data].sort((a, b) => descending(xGetter(a), xGetter(b)));
+        return [...data].sort((a, b) => descending(yGetter(a), yGetter(b)));
       } else if (sortingOrder === "asc" && sortingType === "byDimensionLabel") {
-        return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b)));
+        return [...data].sort((a, b) => ascending(yGetter(a), yGetter(b)));
       } else if (sortingOrder === "desc" && sortingType === "byMeasure") {
         return [...data].sort((a, b) =>
-          descending(getY(a) ?? -1, getY(b) ?? -1)
+          descending(getX(a) ?? -1, getX(b) ?? -1)
         );
       } else if (sortingOrder === "asc" && sortingType === "byMeasure") {
         return [...data].sort((a, b) =>
-          ascending(getY(a) ?? -1, getY(b) ?? -1)
+          ascending(getX(a) ?? -1, getX(b) ?? -1)
         );
       } else {
-        return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b)));
+        return [...data].sort((a, b) => ascending(yGetter(a), yGetter(b)));
       }
     },
-    [getX, getXAsDate, getY, x.sorting, xDimension]
+    [getX, getYAsDate, getY, y.sorting, yDimension]
   );
 
   const getRenderingKey = useRenderingKeyVariable(
@@ -101,9 +101,9 @@ export const useBarsStateVariables = (
   return {
     ...baseVariables,
     sortData,
-    ...bandXVariables,
-    ...numericalYVariables,
-    ...numericalYErrorVariables,
+    ...bandYVariables,
+    ...numericalXVariables,
+    ...numericalXErrorVariables,
     ...interactiveFiltersVariables,
     getRenderingKey,
   };
@@ -114,18 +114,18 @@ export const useBarsStateData = (
   variables: BarsStateVariables
 ): ChartStateData => {
   const { chartConfig, observations } = chartProps;
-  const { sortData, xDimension, getXAsDate, getY, getTimeRangeDate } =
+  const { sortData, yDimension, getYAsDate, getX, getTimeRangeDate } =
     variables;
   const plottableData = usePlottableData(observations, {
-    getY,
+    getX,
   });
   const sortedPlottableData = useMemo(() => {
     return sortData(plottableData);
   }, [sortData, plottableData]);
-  const data = useChartData(sortedPlottableData, {
+  const data = useBarChartData(sortedPlottableData, {
     chartConfig,
-    timeRangeDimensionId: xDimension.id,
-    getXAsDate,
+    timeRangeDimensionId: yDimension.id,
+    getYAsDate,
     getTimeRangeDate,
   });
 
diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
index efd3dfb2e..65d27612e 100644
--- a/app/charts/bar/bars-state.tsx
+++ b/app/charts/bar/bars-state.tsx
@@ -24,9 +24,9 @@ import {
   ChartContext,
   ChartStateData,
   CommonChartState,
-  InteractiveXTimeRangeState,
+  InteractiveYTimeRangeState,
 } from "@/charts/shared/chart-state";
-import { TooltipInfo } from "@/charts/shared/interaction/tooltip";
+import { TooltipInfoInverted } from "@/charts/shared/interaction/tooltip";
 import {
   getCenteredTooltipPlacement,
   MOBILE_TOOLTIP_PLACEMENT,
@@ -51,12 +51,13 @@ import { ChartProps } from "../shared/ChartProps";
 
 export type BarsState = CommonChartState &
   BarsStateVariables &
-  InteractiveXTimeRangeState & {
+  InteractiveYTimeRangeState & {
     chartType: "bar";
-    xScale: ScaleBand<string>;
-    xScaleInteraction: ScaleBand<string>;
-    yScale: ScaleLinear<number, number>;
-    getAnnotationInfo: (d: Observation) => TooltipInfo;
+    xScale: ScaleLinear<number, number>;
+    yScaleInteraction: ScaleBand<string>;
+    yScale: ScaleBand<string>;
+    minY: string;
+    getAnnotationInfo: (d: Observation) => TooltipInfoInverted;
   };
 
 const useBarsState = (
@@ -66,19 +67,19 @@ const useBarsState = (
 ): BarsState => {
   const { chartConfig } = chartProps;
   const {
-    xDimension,
+    yDimension,
     getX,
-    getXAsDate,
-    getXAbbreviationOrLabel,
-    getXLabel,
-    xTimeUnit,
-    yMeasure,
+    getYAsDate,
+    getYAbbreviationOrLabel,
+    getYLabel,
+    yTimeUnit,
+    xMeasure,
     getY,
-    getMinY,
-    showYStandardError,
-    yErrorMeasure,
-    getYError,
-    getYErrorRange,
+    getMinX,
+    showXStandardError,
+    xErrorMeasure,
+    getXError,
+    getXErrorRange,
   } = variables;
   const { chartData, scalesData, timeRangeData, paddingData, allData } = data;
   const { fields, interactiveFiltersConfig } = chartConfig;
@@ -88,12 +89,12 @@ const useBarsState = (
   const formatters = useChartFormatters(chartProps);
   const timeFormatUnit = useTimeFormatUnit();
 
-  const sumsByX = useMemo(() => {
+  const sumsByY = useMemo(() => {
     return Object.fromEntries(
       rollup(
         chartData,
-        (v) => sum(v, (x) => getY(x)),
-        (x) => getX(x)
+        (v) => sum(v, (x) => getX(x)),
+        (x) => getY(x)
       )
     );
   }, [chartData, getX, getY]);
@@ -101,61 +102,62 @@ const useBarsState = (
   const {
     xScale,
     yScale,
+    minY,
     paddingYScale,
-    xScaleTimeRange,
-    xScaleInteraction,
-    xTimeRangeDomainLabels,
+    yScaleTimeRange,
+    yScaleInteraction,
+    yTimeRangeDomainLabels,
   } = useMemo(() => {
-    const sorters = makeDimensionValueSorters(xDimension, {
-      sorting: fields.x.sorting,
-      measureBySegment: sumsByX,
-      useAbbreviations: fields.x.useAbbreviations,
-      dimensionFilter: xDimension?.id
-        ? chartConfig.cubes.find((d) => d.iri === xDimension.cubeIri)?.filters[
-            xDimension.id
+    const sorters = makeDimensionValueSorters(yDimension, {
+      sorting: fields.y.sorting,
+      measureBySegment: sumsByY,
+      useAbbreviations: fields.y.useAbbreviations,
+      dimensionFilter: yDimension?.id
+        ? chartConfig.cubes.find((d) => d.iri === yDimension.cubeIri)?.filters[
+            yDimension.id
           ]
         : undefined,
     });
-    const sortingOrders = getSortingOrders(sorters, fields.x.sorting);
+    const sortingOrders = getSortingOrders(sorters, fields.y.sorting);
     const bandDomain = orderBy(
-      [...new Set(scalesData.map(getX))],
+      [...new Set(scalesData.map(getY))],
       sorters,
       sortingOrders
     );
-    const xTimeRangeValues = [...new Set(timeRangeData.map(getX))];
-    const xTimeRangeDomainLabels = xTimeRangeValues.map(getXLabel);
-    const xScale = scaleBand()
+    const yTimeRangeValues = [...new Set(timeRangeData.map(getY))];
+    const yTimeRangeDomainLabels = yTimeRangeValues.map(getYLabel);
+    const yScale = scaleBand()
       .domain(bandDomain)
       .paddingInner(PADDING_INNER)
       .paddingOuter(PADDING_OUTER);
-    const xScaleInteraction = scaleBand()
+    const yScaleInteraction = scaleBand()
       .domain(bandDomain)
       .paddingInner(0)
       .paddingOuter(0);
 
-    const xScaleTimeRangeDomain = extent(timeRangeData, (d) =>
-      getXAsDate(d)
+    const yScaleTimeRangeDomain = extent(timeRangeData, (d) =>
+      getYAsDate(d)
     ) as [Date, Date];
 
-    const xScaleTimeRange = scaleTime().domain(xScaleTimeRangeDomain);
+    const yScaleTimeRange = scaleTime().domain(yScaleTimeRangeDomain);
 
-    const minValue = getMinY(scalesData, (d) =>
-      getYErrorRange ? getYErrorRange(d)[0] : getY(d)
+    const minValue = getMinX(scalesData, (d) =>
+      getXErrorRange ? getXErrorRange(d)[0] : getX(d)
     );
     const maxValue = Math.max(
       max(scalesData, (d) =>
-        getYErrorRange ? getYErrorRange(d)[1] : getY(d)
+        getXErrorRange ? getXErrorRange(d)[1] : getX(d)
       ) ?? 0,
       0
     );
-    const yScale = scaleLinear().domain([minValue, maxValue]).nice();
+    const xScale = scaleLinear().domain([minValue, maxValue]).nice();
 
-    const paddingMinValue = getMinY(paddingData, (d) =>
-      getYErrorRange ? getYErrorRange(d)[0] : getY(d)
+    const paddingMinValue = getMinX(paddingData, (d) =>
+      getXErrorRange ? getXErrorRange(d)[0] : getX(d)
     );
     const paddingMaxValue = Math.max(
       max(paddingData, (d) =>
-        getYErrorRange ? getYErrorRange(d)[1] : getY(d)
+        getXErrorRange ? getXErrorRange(d)[1] : getX(d)
       ) ?? 0,
       0
     );
@@ -166,26 +168,27 @@ const useBarsState = (
     return {
       xScale,
       yScale,
+      minY: bandDomain[0],
       paddingYScale,
-      xScaleTimeRange,
-      xScaleInteraction,
-      xTimeRangeDomainLabels,
+      yScaleTimeRange,
+      yScaleInteraction,
+      yTimeRangeDomainLabels,
     };
   }, [
     getX,
-    getXLabel,
-    getXAsDate,
+    getYLabel,
+    getYAsDate,
     getY,
-    getYErrorRange,
+    getXErrorRange,
     scalesData,
     paddingData,
     timeRangeData,
-    fields.x.sorting,
-    fields.x.useAbbreviations,
-    xDimension,
+    fields.y.sorting,
+    fields.y.useAbbreviations,
+    yDimension,
     chartConfig.cubes,
-    sumsByX,
-    getMinY,
+    sumsByY,
+    getMinX,
   ]);
 
   const { left, bottom } = useChartPadding({
@@ -195,19 +198,19 @@ const useBarsState = (
     interactiveFiltersConfig,
     animationPresent: !!fields.animation,
     formatNumber,
-    bandDomain: xTimeRangeDomainLabels.every((d) => d === undefined)
-      ? xScale.domain()
-      : xTimeRangeDomainLabels,
+    bandDomain: yTimeRangeDomainLabels.every((d) => d === undefined)
+      ? yScale.domain()
+      : yTimeRangeDomainLabels,
   });
   const right = 40;
-  const { offset: yAxisLabelMargin } = useAxisLabelHeightOffset({
-    label: yMeasure.label,
+  const { offset: xAxisLabelMargin } = useAxisLabelHeightOffset({
+    label: xMeasure.label,
     width,
     marginLeft: left,
     marginRight: right,
   });
   const margins = {
-    top: 50 + yAxisLabelMargin,
+    top: 50 + xAxisLabelMargin,
     right,
     bottom,
     left,
@@ -217,53 +220,53 @@ const useBarsState = (
   const { chartWidth, chartHeight } = bounds;
 
   xScale.range([0, chartWidth]);
-  xScaleInteraction.range([0, chartWidth]);
-  xScaleTimeRange.range([0, chartWidth]);
+  yScaleInteraction.range([0, chartHeight]);
+  yScaleTimeRange.range([0, chartHeight]);
   yScale.range([chartHeight, 0]);
 
   const isMobile = useIsMobile();
 
   // Tooltip
-  const getAnnotationInfo = (d: Observation): TooltipInfo => {
-    const xAnchor = (xScale(getX(d)) as number) + xScale.bandwidth() * 0.5;
-    const yAnchor = isMobile
+  const getAnnotationInfo = (d: Observation): TooltipInfoInverted => {
+    const yAnchor = (yScale(getY(d)) as number) + yScale.bandwidth() * 0.5;
+    const xAnchor = isMobile
       ? chartHeight
-      : yScale(Math.max(getY(d) ?? NaN, 0));
+      : xScale(Math.max(getX(d) ?? NaN, 0));
     const placement = isMobile
       ? MOBILE_TOOLTIP_PLACEMENT
       : getCenteredTooltipPlacement({
           chartWidth,
-          xAnchor,
+          xAnchor: yAnchor,
           topAnchor: !fields.segment,
         });
 
-    const xLabel = getXAbbreviationOrLabel(d);
+    const yLabel = getYAbbreviationOrLabel(d);
 
-    const yValueFormatter = (value: number | null) =>
+    const xValueFormatter = (value: number | null) =>
       formatNumberWithUnit(
         value,
-        formatters[yMeasure.id] ?? formatNumber,
-        yMeasure.unit
+        formatters[xMeasure.id] ?? formatNumber,
+        xMeasure.unit
       );
 
     const getError = (d: Observation) => {
-      if (!showYStandardError || !getYError || getYError(d) === null) {
+      if (!showXStandardError || !getXError || getXError(d) === null) {
         return;
       }
 
-      return `${getYError(d)}${yErrorMeasure?.unit ?? ""}`;
+      return `${getXError(d)}${xErrorMeasure?.unit ?? ""}`;
     };
 
-    const y = getY(d);
+    const x = getX(d);
 
     return {
       xAnchor,
       yAnchor,
       placement,
-      xValue: xTimeUnit ? timeFormatUnit(xLabel, xTimeUnit) : xLabel,
+      yValue: yTimeUnit ? timeFormatUnit(yLabel, yTimeUnit) : yLabel,
       datum: {
         label: undefined,
-        value: y !== null && isNaN(y) ? "-" : `${yValueFormatter(getY(d))}`,
+        value: x !== null && isNaN(x) ? "-" : `${xValueFormatter(getX(d))}`,
         error: getError(d),
         color: "",
       },
@@ -277,8 +280,9 @@ const useBarsState = (
     chartData,
     allData,
     xScale,
-    xScaleTimeRange,
-    xScaleInteraction,
+    minY,
+    yScaleTimeRange,
+    yScaleInteraction,
     yScale,
     getAnnotationInfo,
     ...variables,
diff --git a/app/charts/bar/bars.tsx b/app/charts/bar/bars.tsx
index 810b30026..e7099e1d2 100644
--- a/app/charts/bar/bars.tsx
+++ b/app/charts/bar/bars.tsx
@@ -5,54 +5,54 @@ import { BarsState } from "@/charts/bar/bars-state";
 import { RenderBarDatum, renderBars } from "@/charts/bar/rendering-utils";
 import { useChartState } from "@/charts/shared/chart-state";
 import {
-  RenderWhiskerDatum,
+  RenderWhiskerBarDatum,
   filterWithoutErrors,
+  renderBarWhiskers,
   renderContainer,
-  renderWhiskers,
 } from "@/charts/shared/rendering-utils";
 import { useTransitionStore } from "@/stores/transition";
 import { useTheme } from "@/themes";
 
 export const ErrorWhiskers = () => {
   const {
-    getX,
-    getYError,
-    getYErrorRange,
+    getY,
+    getXError,
+    getXErrorRange,
     chartData,
     yScale,
     xScale,
-    showYStandardError,
+    showXStandardError,
     bounds,
   } = useChartState() as BarsState;
   const { margins, width, height } = bounds;
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
-  const renderData: RenderWhiskerDatum[] = useMemo(() => {
-    if (!getYErrorRange || !showYStandardError) {
+  const renderData: RenderWhiskerBarDatum[] = useMemo(() => {
+    if (!getXErrorRange || !showXStandardError) {
       return [];
     }
 
-    const bandwidth = xScale.bandwidth();
-    return chartData.filter(filterWithoutErrors(getYError)).map((d, i) => {
-      const x0 = xScale(getX(d)) as number;
+    const bandwidth = yScale.bandwidth();
+    return chartData.filter(filterWithoutErrors(getXError)).map((d, i) => {
+      const y0 = yScale(getY(d)) as number;
       const barWidth = Math.min(bandwidth, 15);
-      const [y1, y2] = getYErrorRange(d);
+      const [x1, x2] = getXErrorRange(d);
       return {
         key: `${i}`,
-        x: x0 + bandwidth / 2 - barWidth / 2,
-        y1: yScale(y1),
-        y2: yScale(y2),
+        y: y0 + bandwidth / 2 - barWidth / 2,
+        x1: xScale(x1),
+        x2: xScale(x2),
         width: barWidth,
-      } as RenderWhiskerDatum;
+      } as RenderWhiskerBarDatum;
     });
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [
     chartData,
-    getX,
-    getYError,
-    getYErrorRange,
-    showYStandardError,
+    getY,
+    getXError,
+    getXErrorRange,
+    showXStandardError,
     xScale,
     yScale,
     width,
@@ -65,7 +65,7 @@ export const ErrorWhiskers = () => {
         id: "bars-error-whiskers",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g, opts) => renderWhiskers(g, renderData, opts),
+        render: (g, opts) => renderBarWhiskers(g, renderData, opts),
       });
     }
   }, [
@@ -87,8 +87,8 @@ export const Bars = () => {
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
-  const bandwidth = xScale.bandwidth();
-  const y0 = yScale(0);
+  const bandwidth = yScale.bandwidth();
+  const x0 = xScale(0);
   const renderData: RenderBarDatum[] = useMemo(() => {
     const getColor = (d: number) => {
       return d <= 0 ? theme.palette.secondary.main : schemeCategory10[0];
@@ -96,20 +96,20 @@ export const Bars = () => {
 
     return chartData.map((d) => {
       const key = getRenderingKey(d);
-      const xScaled = xScale(getX(d)) as number;
-      const yRaw = getY(d);
-      const y = yRaw === null || isNaN(yRaw) ? 0 : yRaw;
-      const yScaled = yScale(y);
-      const yRender = yScale(Math.max(y, 0));
-      const height = Math.max(0, Math.abs(yScaled - y0));
-      const color = getColor(y);
+      const yScaled = yScale(getY(d)) as number;
+      const xRaw = getX(d);
+      const x = xRaw === null || isNaN(xRaw) ? 0 : xRaw;
+      const xScaled = xScale(x);
+      const xRender = xScale(Math.min(x, 0));
+      const width = Math.max(0, Math.abs(xScaled - x0));
+      const color = getColor(x);
 
       return {
         key,
-        x: xScaled,
-        y: yRender,
-        width: bandwidth,
-        height,
+        x: xRender,
+        y: yScaled,
+        width,
+        height: bandwidth,
         color,
       };
     });
@@ -120,7 +120,7 @@ export const Bars = () => {
     getY,
     xScale,
     yScale,
-    y0,
+    x0,
     theme.palette.secondary.main,
     getRenderingKey,
   ]);
@@ -131,7 +131,7 @@ export const Bars = () => {
         id: "bars",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g, opts) => renderBars(g, renderData, { ...opts, y0 }),
+        render: (g, opts) => renderBars(g, renderData, { ...opts, x0 }),
       });
     }
   }, [
@@ -140,7 +140,7 @@ export const Bars = () => {
     margins.top,
     renderData,
     transitionDuration,
-    y0,
+    x0,
   ]);
 
   return <g ref={ref} />;
diff --git a/app/charts/bar/chart-bar.tsx b/app/charts/bar/chart-bar.tsx
index 215b657c2..05ccdd948 100644
--- a/app/charts/bar/chart-bar.tsx
+++ b/app/charts/bar/chart-bar.tsx
@@ -11,11 +11,11 @@ import { StackedBarsChart } from "@/charts/bar/bars-stacked-state";
 import { BarChart } from "@/charts/bar/bars-state";
 import { InteractionBars } from "@/charts/bar/overlay-bars";
 import { ChartDataWrapper } from "@/charts/chart-data-wrapper";
-import { AxisHeightLinear } from "@/charts/shared/axis-height-linear";
 import {
   AxisWidthBand,
   AxisWidthBandDomain,
-} from "@/charts/shared/axis-width-band";
+} from "@/charts/shared/axis-width-band-vertical";
+import { AxisWidthLinear } from "@/charts/shared/axis-width-linear";
 import { BrushTime, shouldShowBrush } from "@/charts/shared/brush";
 import {
   ChartContainer,
@@ -53,7 +53,7 @@ const ChartBars = memo((props: ChartProps<BarConfig>) => {
         <StackedBarsChart {...props}>
           <ChartContainer>
             <ChartSvg>
-              <AxisHeightLinear />
+              <AxisWidthLinear />
               <AxisWidthBand />
               <BarsStacked />
               <AxisWidthBandDomain />
@@ -83,7 +83,7 @@ const ChartBars = memo((props: ChartProps<BarConfig>) => {
         <GroupedBarChart {...props}>
           <ChartContainer>
             <ChartSvg>
-              <AxisHeightLinear />
+              <AxisWidthLinear />
               <AxisWidthBand />
               <BarsGrouped />
               <ErrorWhiskersGrouped />
@@ -114,7 +114,7 @@ const ChartBars = memo((props: ChartProps<BarConfig>) => {
         <BarChart {...props}>
           <ChartContainer>
             <ChartSvg>
-              <AxisHeightLinear />
+              <AxisWidthLinear />
               <AxisWidthBand />
               <Bars />
               <ErrorWhiskers />
diff --git a/app/charts/bar/overlay-bars.tsx b/app/charts/bar/overlay-bars.tsx
index c919f2a41..0850d2d30 100644
--- a/app/charts/bar/overlay-bars.tsx
+++ b/app/charts/bar/overlay-bars.tsx
@@ -1,5 +1,4 @@
-import { ColumnsState } from "@/charts/column/columns-state";
-import { ComboLineColumnState } from "@/charts/combo/combo-line-column-state";
+import { BarsState } from "@/charts/bar/bars-state";
 import { useChartState } from "@/charts/shared/chart-state";
 import { useInteraction } from "@/charts/shared/use-interaction";
 import { Observation } from "@/domain/data";
@@ -7,10 +6,9 @@ import { Observation } from "@/domain/data";
 export const InteractionBars = () => {
   const [, dispatch] = useInteraction();
 
-  const { chartData, bounds, getX, xScaleInteraction } = useChartState() as
-    | ColumnsState
-    | ComboLineColumnState;
-  const { margins, chartHeight } = bounds;
+  const { chartData, bounds, getY, yScaleInteraction } =
+    useChartState() as BarsState;
+  const { margins, chartWidth } = bounds;
 
   const showTooltip = (d: Observation) => {
     dispatch({
@@ -28,10 +26,10 @@ export const InteractionBars = () => {
       {chartData.map((d, i) => (
         <rect
           key={i}
-          x={xScaleInteraction(getX(d)) as number}
-          y={0}
-          width={xScaleInteraction.bandwidth()}
-          height={Math.max(0, chartHeight)}
+          x={0}
+          y={yScaleInteraction(getY(d)) as number}
+          height={yScaleInteraction.bandwidth()}
+          width={Math.max(0, chartWidth)}
           fill="hotpink"
           fillOpacity={0}
           stroke="none"
diff --git a/app/charts/bar/rendering-utils.ts b/app/charts/bar/rendering-utils.ts
index 67123ec61..1e7cbb104 100644
--- a/app/charts/bar/rendering-utils.ts
+++ b/app/charts/bar/rendering-utils.ts
@@ -15,7 +15,7 @@ export type RenderBarDatum = {
 };
 
 type RenderBarOptions = RenderOptions & {
-  y0: number;
+  x0: number;
 };
 
 export const renderBars = (
@@ -23,7 +23,7 @@ export const renderBars = (
   data: RenderBarDatum[],
   options: RenderBarOptions
 ) => {
-  const { transition, y0 } = options;
+  const { transition, x0 } = options;
 
   g.selectAll<SVGRectElement, RenderBarDatum>("rect")
     .data(data, (d) => d.key)
@@ -32,15 +32,15 @@ export const renderBars = (
         enter
           .append("rect")
           .attr("data-index", (_, i) => i)
-          .attr("x", (d) => d.x)
-          .attr("y", y0)
+          .attr("y", (d) => d.y)
+          .attr("x", x0)
           .attr("width", (d) => d.width)
           .attr("height", 0)
           .attr("fill", (d) => d.color)
           .call((enter) =>
             maybeTransition(enter, {
               transition,
-              s: (g) => g.attr("y", (d) => d.y).attr("height", (d) => d.height),
+              s: (g) => g.attr("x", (d) => d.x).attr("width", (d) => d.width),
             })
           ),
       (update) =>
@@ -57,7 +57,7 @@ export const renderBars = (
       (exit) =>
         maybeTransition(exit, {
           transition,
-          s: (g) => g.attr("y", y0).attr("height", 0).remove(),
+          s: (g) => g.attr("x", x0).attr("height", 0).remove(),
         })
     );
 };
diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts
index b4d544c1b..bc96a5cfe 100644
--- a/app/charts/chart-config-ui-options.ts
+++ b/app/charts/chart-config-ui-options.ts
@@ -773,7 +773,7 @@ const chartConfigOptionsUISpec: ChartSpecs = {
     chartType: "bar",
     encodings: [
       {
-        field: "y",
+        field: "x",
         optional: false,
         idAttributes: ["componentId"],
         componentTypes: ["NumericalMeasure"],
@@ -801,7 +801,7 @@ const chartConfigOptionsUISpec: ChartSpecs = {
         },
       },
       {
-        field: "x",
+        field: "y",
         optional: false,
         idAttributes: ["componentId"],
         componentTypes: [
diff --git a/app/charts/index.ts b/app/charts/index.ts
index 9e667d844..1430ed1ae 100644
--- a/app/charts/index.ts
+++ b/app/charts/index.ts
@@ -444,11 +444,11 @@ export const getInitialConfig = (
           timeRangeComponentId: barXComponentId,
         }),
         fields: {
-          x: {
+          y: {
             componentId: barXComponentId,
             sorting: DEFAULT_SORTING,
           },
-          y: { componentId: numericalMeasures[0].id },
+          x: { componentId: numericalMeasures[0].id },
         },
       };
     case "line":
@@ -1019,7 +1019,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
     },
     fields: {
       x: {
-        componentId: ({ oldValue, newChartConfig, dimensions }) => {
+        componentId: ({ oldValue, newChartConfig, dimensions, measures }) => {
+          measures[0];
           // When switching from a scatterplot, x is a measure.
           if (dimensions.find((d) => d.id === oldValue)) {
             return produce(newChartConfig, (draft) => {
@@ -1031,7 +1032,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
         },
       },
       y: {
-        componentId: ({ oldValue, newChartConfig }) => {
+        componentId: ({ oldValue, newChartConfig, dimensions }) => {
+          dimensions[0];
           return produce(newChartConfig, (draft) => {
             draft.fields.y.componentId = oldValue;
           });
diff --git a/app/charts/shared/axis-width-band-vertical.tsx b/app/charts/shared/axis-width-band-vertical.tsx
new file mode 100644
index 000000000..b8a3d32b0
--- /dev/null
+++ b/app/charts/shared/axis-width-band-vertical.tsx
@@ -0,0 +1,134 @@
+import { axisLeft } from "d3-axis";
+import { useEffect, useRef } from "react";
+
+import { BarsState } from "@/charts/bar/bars-state";
+import { useChartState } from "@/charts/shared/chart-state";
+import {
+  maybeTransition,
+  renderContainer,
+} from "@/charts/shared/rendering-utils";
+import { useChartTheme } from "@/charts/shared/use-chart-theme";
+import { useTimeFormatUnit } from "@/formatters";
+import { useTransitionStore } from "@/stores/transition";
+
+export const AxisWidthBand = () => {
+  const ref = useRef<SVGGElement>(null);
+  const state = useChartState() as BarsState;
+  const { xScale, getYLabel, yTimeUnit, yScale, bounds } = state;
+  const enableTransition = useTransitionStore((state) => state.enable);
+  const transitionDuration = useTransitionStore((state) => state.duration);
+  const formatDate = useTimeFormatUnit();
+  const { chartHeight, margins } = bounds;
+  const { labelColor, gridColor, labelFontSize, fontFamily, domainColor } =
+    useChartTheme();
+
+  useEffect(() => {
+    if (ref.current) {
+      const rotation = true;
+      const hasNegativeValues = xScale.domain()[0] < 0;
+      const fontSize =
+        yScale.bandwidth() > labelFontSize ? labelFontSize : yScale.bandwidth();
+      const axis = axisLeft(yScale)
+        .tickSizeOuter(0)
+        .tickSizeInner(hasNegativeValues ? -chartHeight : 6)
+        .tickPadding(rotation ? -10 : 0);
+
+      if (yTimeUnit) {
+        axis.tickFormat((d) => formatDate(d, yTimeUnit));
+      } else {
+        axis.tickFormat((d) => getYLabel(d));
+      }
+
+      const g = renderContainer(ref.current, {
+        id: "axis-width-band-vertical",
+        transform: `translate(${margins.left} ${margins.top})`,
+        transition: { enable: enableTransition, duration: transitionDuration },
+        render: (g) => g.attr("data-testid", "axis-width-band").call(axis),
+        renderUpdate: (g, opts) =>
+          maybeTransition(g, {
+            transition: opts.transition,
+            s: (g) => g.call(axis),
+          }),
+      });
+
+      g.select(".domain").remove();
+      g.selectAll(".tick line").attr(
+        "stroke",
+        hasNegativeValues ? gridColor : domainColor
+      );
+      g.selectAll(".tick text")
+        .attr("transform", rotation ? "rotate(90)" : "rotate(0)")
+        .attr("x", rotation ? fontSize : 0)
+        .attr("font-size", fontSize)
+        .attr("font-family", fontFamily)
+        .attr("fill", labelColor)
+        .attr("text-anchor", rotation ? "start" : "unset");
+    }
+  }, [
+    chartHeight,
+    domainColor,
+    enableTransition,
+    fontFamily,
+    formatDate,
+    getYLabel,
+    gridColor,
+    labelColor,
+    labelFontSize,
+    margins.left,
+    margins.top,
+    transitionDuration,
+    xScale,
+    yTimeUnit,
+    yScale,
+  ]);
+
+  return <g ref={ref} />;
+};
+
+export const AxisWidthBandDomain = () => {
+  const ref = useRef<SVGGElement>(null);
+  const enableTransition = useTransitionStore((state) => state.enable);
+  const transitionDuration = useTransitionStore((state) => state.duration);
+  const { xScale, yScale, bounds, minY } = useChartState() as BarsState;
+  const { chartHeight, margins } = bounds;
+  const { domainColor } = useChartTheme();
+
+  useEffect(() => {
+    if (ref.current) {
+      const axis = axisLeft(yScale).tickSizeOuter(0);
+      const g = renderContainer(ref.current, {
+        id: "axis-width-band-vertical-domain",
+        transform: `translate(${margins.left} ${margins.top})`,
+        transition: { enable: enableTransition, duration: transitionDuration },
+        render: (g) => g.call(axis),
+        renderUpdate: (g, opts) =>
+          maybeTransition(g, {
+            transition: opts.transition,
+            s: (g) => g.call(axis),
+          }),
+      });
+
+      g.selectAll(".tick line").remove();
+      g.selectAll(".tick text").remove();
+      g.select(".domain")
+        .attr(
+          "transform",
+          `translate(0, -${chartHeight - (yScale(minY) || 0)})`
+        )
+        .attr("stroke", domainColor);
+    }
+  }, [
+    minY,
+    bounds.chartHeight,
+    chartHeight,
+    domainColor,
+    enableTransition,
+    margins.left,
+    margins.top,
+    transitionDuration,
+    xScale,
+    yScale,
+  ]);
+
+  return <g ref={ref} />;
+};
diff --git a/app/charts/shared/axis-width-linear.tsx b/app/charts/shared/axis-width-linear.tsx
index d092320fa..ee53f38fb 100644
--- a/app/charts/shared/axis-width-linear.tsx
+++ b/app/charts/shared/axis-width-linear.tsx
@@ -1,6 +1,7 @@
 import { axisBottom } from "d3-axis";
 import { useEffect, useRef } from "react";
 
+import { BarsState } from "@/charts/bar/bars-state";
 import { ScatterplotState } from "@/charts/scatterplot/scatterplot-state";
 import { useAxisLabelHeightOffset } from "@/charts/shared/chart-dimensions";
 import { useChartState } from "@/charts/shared/chart-state";
@@ -16,8 +17,9 @@ import { getTextWidth } from "@/utils/get-text-width";
 
 export const AxisWidthLinear = () => {
   const formatNumber = useFormatNumber();
-  const { xScale, bounds, xAxisLabel, xMeasure } =
-    useChartState() as ScatterplotState;
+  const { xScale, bounds, xAxisLabel, xMeasure } = useChartState() as
+    | ScatterplotState
+    | BarsState;
   const { chartWidth, chartHeight, margins } = bounds;
   const {
     labelColor,
diff --git a/app/charts/shared/chart-helpers.tsx b/app/charts/shared/chart-helpers.tsx
index 1a9ca605d..08390a218 100644
--- a/app/charts/shared/chart-helpers.tsx
+++ b/app/charts/shared/chart-helpers.tsx
@@ -6,6 +6,7 @@ import { useCallback, useMemo } from "react";
 import { useMaybeAbbreviations } from "@/charts/shared/abbreviations";
 import {
   imputeTemporalLinearSeries,
+  imputeTemporalLinearSeriesInverted,
   interpolateZerosValue,
 } from "@/charts/shared/imputation";
 import { useObservationLabels } from "@/charts/shared/observation-labels";
@@ -486,6 +487,88 @@ export const getWideData = ({
   }
 };
 
+export const getWideDataInverted = ({
+  dataGroupedByY,
+  yKey,
+  getX,
+  allSegments,
+  getSegment,
+  imputationType = "none",
+}: {
+  dataGroupedByY: InternMap<string, Array<Observation>>;
+  yKey: string;
+  getX: (d: Observation) => number | null;
+  allSegments?: Array<string>;
+  getSegment: (d: Observation) => string;
+  imputationType?: ImputationType;
+}) => {
+  switch (imputationType) {
+    case "linear":
+      if (allSegments) {
+        const dataGroupedByYEntries = [...dataGroupedByY.entries()];
+        const dataGroupedByYWithImputedValues: Array<{
+          [key: string]: number;
+        }> = Array.from({ length: dataGroupedByY.size }, () => ({}));
+
+        for (const segment of allSegments) {
+          const imputedSeriesValues = imputeTemporalLinearSeriesInverted({
+            dataSortedByY: dataGroupedByYEntries.map(([date, values]) => {
+              const observation = values.find((d) => getSegment(d) === segment);
+
+              return {
+                date: new Date(date),
+                value: observation ? getX(observation) : null,
+              };
+            }),
+          });
+
+          for (let i = 0; i < imputedSeriesValues.length; i++) {
+            dataGroupedByYWithImputedValues[i][segment] =
+              imputedSeriesValues[i].value;
+          }
+        }
+
+        return getBaseWideDataInverted({
+          dataGroupedByY,
+          yKey,
+          getX,
+          getSegment,
+          getOptionalObservationProps: (i) => {
+            return allSegments.map((d) => {
+              return {
+                [d]: dataGroupedByYWithImputedValues[i][d],
+              };
+            });
+          },
+        });
+      }
+    case "zeros":
+      if (allSegments) {
+        return getBaseWideDataInverted({
+          dataGroupedByY,
+          yKey,
+          getX,
+          getSegment,
+          getOptionalObservationProps: () => {
+            return allSegments.map((d) => {
+              return {
+                [d]: interpolateZerosValue(),
+              };
+            });
+          },
+        });
+      }
+    case "none":
+    default:
+      return getBaseWideDataInverted({
+        dataGroupedByY,
+        yKey,
+        getX,
+        getSegment,
+      });
+  }
+};
+
 const getBaseWideData = ({
   dataGroupedByX,
   xKey,
@@ -533,6 +616,53 @@ const getBaseWideData = ({
   return wideData;
 };
 
+const getBaseWideDataInverted = ({
+  dataGroupedByY,
+  yKey,
+  getX,
+  getSegment,
+  getOptionalObservationProps = () => [],
+}: {
+  dataGroupedByY: InternMap<string, Array<Observation>>;
+  yKey: string;
+  getX: (d: Observation) => number | null;
+  getSegment: (d: Observation) => string;
+  getOptionalObservationProps?: (
+    datumIndex: number
+  ) => Array<{ [key: string]: number }>;
+}): Array<Observation> => {
+  const wideData = [];
+  const dataGroupedByYEntries = [...dataGroupedByY.entries()];
+
+  for (let i = 0; i < dataGroupedByY.size; i++) {
+    const [k, v] = dataGroupedByYEntries[i];
+
+    const observation: Observation = Object.assign(
+      {
+        [yKey]: k,
+        [`${yKey}/__iri__`]: v[0][`${yKey}/__iri__`],
+        total: sum(v, getX),
+      },
+      ...getOptionalObservationProps(i),
+      ...v
+        // Sorting the values in case of multiple values for the same segment
+        // (desired behavior for getting the domain when time slider is active).
+        .sort((a, b) => {
+          return (getX(a) ?? 0) - (getX(b) ?? 0);
+        })
+        .map((d) => {
+          return {
+            [getSegment(d)]: getX(d),
+          };
+        })
+    );
+
+    wideData.push(observation);
+  }
+
+  return wideData;
+};
+
 const getIdentityId = (id: string) => `${id}/__identity__`;
 export const useGetIdentityY = (id: string) => {
   return useCallback(
@@ -542,6 +672,14 @@ export const useGetIdentityY = (id: string) => {
     [id]
   );
 };
+export const useGetIdentityX = (id: string) => {
+  return useCallback(
+    (d: Observation) => {
+      return (d[getIdentityId(id)] as number | null) ?? null;
+    },
+    [id]
+  );
+};
 
 export const normalizeData = (
   sortedData: Observation[],
@@ -567,6 +705,30 @@ export const normalizeData = (
   });
 };
 
+export const normalizeDataInverted = (
+  sortedData: Observation[],
+  {
+    xKey,
+    getX,
+    getTotalGroupValue,
+  }: {
+    xKey: string;
+    getX: (d: Observation) => number | null;
+    getTotalGroupValue: (d: Observation) => number;
+  }
+): Observation[] => {
+  return sortedData.map((d) => {
+    const totalGroupValue = getTotalGroupValue(d);
+    const x = getX(d);
+
+    return {
+      ...d,
+      [xKey]: 100 * (x ? x / totalGroupValue : x ?? 0),
+      [getIdentityId(xKey)]: x,
+    };
+  });
+};
+
 const SlugRe = /\W+/g;
 export const getSlugifiedId = (id: string) => id.replace(SlugRe, "_");
 
diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts
index 871a9f369..2d01838ef 100644
--- a/app/charts/shared/chart-state.ts
+++ b/app/charts/shared/chart-state.ts
@@ -109,7 +109,7 @@ export const useChartState = () => {
 export type ChartWithInteractiveXTimeRangeState =
   | AreasState
   | ColumnsState
-  | BarsState
+  // | BarsState
   | LinesState;
 
 export type NumericalValueGetter = (d: Observation) => number | null;
@@ -147,6 +147,15 @@ export const useBaseVariables = (chartConfig: ChartConfig): BaseVariables => {
   };
 };
 
+export type BandYVariables = {
+  yDimension: Dimension;
+  getY: StringValueGetter;
+  getYLabel: (d: string) => string;
+  getYAbbreviationOrLabel: (d: Observation) => string;
+  yTimeUnit: TimeUnit | undefined;
+  getYAsDate: TemporalValueGetter;
+};
+
 export type BandXVariables = {
   xDimension: Dimension;
   getX: StringValueGetter;
@@ -156,6 +165,51 @@ export type BandXVariables = {
   getXAsDate: TemporalValueGetter;
 };
 
+export const useBandYVariables = (
+  y: GenericField,
+  {
+    dimensionsById,
+    observations,
+  }: {
+    dimensionsById: DimensionsById;
+    observations: Observation[];
+  }
+): BandYVariables => {
+  const yDimension = dimensionsById[y.componentId];
+  if (!yDimension) {
+    throw Error(`No dimension <${y.componentId}> in cube! (useBandXVariables)`);
+  }
+
+  const yTimeUnit = isTemporalDimension(yDimension)
+    ? yDimension.timeUnit
+    : undefined;
+
+  const {
+    getAbbreviationOrLabelByValue: getYAbbreviationOrLabel,
+    getValue: getY,
+    getLabel: getYLabel,
+  } = useDimensionWithAbbreviations(yDimension, {
+    observations,
+    field: y,
+  });
+
+  const getYAsDate = useTemporalVariable(y.componentId);
+  const getYTemporalEntity = useTemporalEntityVariable(
+    dimensionsById[y.componentId].values
+  )(y.componentId);
+
+  return {
+    yDimension,
+    getY,
+    getYLabel,
+    getYAbbreviationOrLabel,
+    yTimeUnit,
+    getYAsDate: isTemporalDimension(yDimension)
+      ? getYAsDate
+      : getYTemporalEntity,
+  };
+};
+
 export const useBandXVariables = (
   x: GenericField,
   {
@@ -252,7 +306,7 @@ export type NumericalXVariables = {
 };
 
 export const useNumericalXVariables = (
-  chartType: "scatterplot",
+  chartType: "scatterplot" | "bar",
   x: GenericField,
   { measuresById }: { measuresById: MeasuresById }
 ): NumericalXVariables => {
@@ -273,6 +327,7 @@ export const useNumericalXVariables = (
     (data: Observation[], _getX: NumericalValueGetter) => {
       switch (chartType) {
         case "scatterplot":
+        case "bar":
           return shouldUseDynamicMinScaleValue(xMeasure.scaleType)
             ? min(data, _getX) ?? 0
             : Math.min(0, min(data, _getX) ?? 0);
@@ -356,6 +411,13 @@ export type NumericalYErrorVariables = {
   getYErrorRange: null | ((d: Observation) => [number, number]);
 };
 
+export type NumericalXErrorVariables = {
+  showXStandardError: boolean;
+  xErrorMeasure: Component | undefined;
+  getXError: ((d: Observation) => ObservationValue) | null;
+  getXErrorRange: null | ((d: Observation) => [number, number]);
+};
+
 export const useNumericalYErrorVariables = (
   y: GenericField,
   {
@@ -384,6 +446,34 @@ export const useNumericalYErrorVariables = (
   };
 };
 
+export const useNumericalXErrorVariables = (
+  x: GenericField,
+  {
+    numericalXVariables,
+    dimensions,
+    measures,
+  }: {
+    numericalXVariables: NumericalXVariables;
+    dimensions: Dimension[];
+    measures: Measure[];
+  }
+): NumericalXErrorVariables => {
+  const showXStandardError = get(x, ["showStandardError"], true);
+  const xErrorMeasure = useErrorMeasure(x.componentId, {
+    dimensions,
+    measures,
+  });
+  const getXErrorRange = useErrorRange(xErrorMeasure, numericalXVariables.getX);
+  const getXError = useErrorVariable(xErrorMeasure);
+
+  return {
+    showXStandardError,
+    xErrorMeasure,
+    getXError,
+    getXErrorRange,
+  };
+};
+
 export type SegmentVariables = {
   segmentDimension: Dimension | undefined;
   segmentsByAbbreviationOrLabel: Map<string, DimensionValue>;
@@ -657,7 +747,186 @@ export const useChartData = (
   };
 };
 
+export const useBarChartData = (
+  observations: Observation[],
+  {
+    chartConfig,
+    timeRangeDimensionId,
+    getYAsDate,
+    getSegmentAbbreviationOrLabel,
+    getTimeRangeDate,
+  }: {
+    chartConfig: ChartConfig;
+    timeRangeDimensionId: string | undefined;
+    getYAsDate?: (d: Observation) => Date;
+    getSegmentAbbreviationOrLabel?: (d: Observation) => string;
+    getTimeRangeDate?: (d: Observation) => Date;
+  }
+): Omit<ChartStateData, "allData"> => {
+  const { interactiveFiltersConfig } = chartConfig;
+  const categories = useChartInteractiveFilters((d) => d.categories);
+  const timeRange = useChartInteractiveFilters((d) => d.timeRange);
+  const timeSlider = useChartInteractiveFilters((d) => d.timeSlider);
+
+  // time range
+  const interactiveTimeRange = interactiveFiltersConfig?.timeRange;
+  const timeRangeFromTime = interactiveTimeRange?.presets.from
+    ? parseDate(interactiveTimeRange?.presets.from).getTime()
+    : undefined;
+  const timeRangeToTime = interactiveTimeRange?.presets.to
+    ? parseDate(interactiveTimeRange?.presets.to).getTime()
+    : undefined;
+  const timeRangeFilters = useMemo(() => {
+    const timeRangeFilter: ValuePredicate | null =
+      getTimeRangeDate && timeRangeFromTime && timeRangeToTime
+        ? (d: Observation) => {
+            const time = getTimeRangeDate(d).getTime();
+            return time >= timeRangeFromTime && time <= timeRangeToTime;
+          }
+        : null;
+
+    return timeRangeFilter ? [timeRangeFilter] : [];
+  }, [timeRangeFromTime, timeRangeToTime, getTimeRangeDate]);
+
+  // interactive time range
+  const interactiveFromTime = timeRange.from?.getTime();
+  const interactiveToTime = timeRange.to?.getTime();
+  const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs);
+  const { potentialTimeRangeFilterIds } = useDashboardInteractiveFilters();
+  const interactiveTimeRangeFilters = useMemo(() => {
+    const interactiveTimeRangeFilter: ValuePredicate | null =
+      getYAsDate &&
+      interactiveFromTime &&
+      interactiveToTime &&
+      (interactiveTimeRange?.active ||
+        (dashboardFilters?.timeRange.active &&
+          timeRangeDimensionId &&
+          potentialTimeRangeFilterIds.includes(timeRangeDimensionId)))
+        ? (d: Observation) => {
+            const time = getYAsDate(d).getTime();
+            return time >= interactiveFromTime && time <= interactiveToTime;
+          }
+        : null;
+
+    return interactiveTimeRangeFilter ? [interactiveTimeRangeFilter] : [];
+  }, [
+    getYAsDate,
+    interactiveFromTime,
+    interactiveToTime,
+    interactiveTimeRange?.active,
+    dashboardFilters?.timeRange.active,
+    timeRangeDimensionId,
+    potentialTimeRangeFilterIds,
+  ]);
+
+  // interactive time slider
+  const animationField = getAnimationField(chartConfig);
+  const dynamicScales = animationField?.dynamicScales ?? true;
+  const animationComponentId = animationField?.componentId ?? "";
+  const getAnimationDate = useTemporalVariable(animationComponentId);
+  const getAnimationOrdinalDate = useStringVariable(animationComponentId);
+  const interactiveTimeSliderFilters = useMemo(() => {
+    const interactiveTimeSliderFilter: ValuePredicate | null =
+      animationField?.componentId && timeSlider.value
+        ? (d: Observation) => {
+            if (timeSlider.type === "interval") {
+              return (
+                getAnimationDate(d).getTime() === timeSlider.value!.getTime()
+              );
+            }
+
+            const ordinalDate = getAnimationOrdinalDate(d);
+            return ordinalDate === timeSlider.value!;
+          }
+        : null;
+
+    return interactiveTimeSliderFilter ? [interactiveTimeSliderFilter] : [];
+  }, [
+    animationField?.componentId,
+    timeSlider.type,
+    timeSlider.value,
+    getAnimationDate,
+    getAnimationOrdinalDate,
+  ]);
+
+  // interactive legend
+  const interactiveLegendFilters = useMemo(() => {
+    const legendItems = Object.keys(categories);
+    const interactiveLegendFilter: ValuePredicate | null =
+      interactiveFiltersConfig?.legend?.active && getSegmentAbbreviationOrLabel
+        ? (d: Observation) => {
+            return !legendItems.includes(getSegmentAbbreviationOrLabel(d));
+          }
+        : null;
+
+    return interactiveLegendFilter ? [interactiveLegendFilter] : [];
+  }, [
+    categories,
+    getSegmentAbbreviationOrLabel,
+    interactiveFiltersConfig?.legend?.active,
+  ]);
+
+  const chartData = useMemo(() => {
+    return observations.filter(
+      overEvery([
+        ...interactiveLegendFilters,
+        ...interactiveTimeRangeFilters,
+        ...interactiveTimeSliderFilters,
+      ])
+    );
+  }, [
+    observations,
+    interactiveLegendFilters,
+    interactiveTimeRangeFilters,
+    interactiveTimeSliderFilters,
+  ]);
+
+  const scalesData = useMemo(() => {
+    if (dynamicScales) {
+      return chartData;
+    } else {
+      return observations.filter(
+        overEvery([...interactiveLegendFilters, ...interactiveTimeRangeFilters])
+      );
+    }
+  }, [
+    dynamicScales,
+    chartData,
+    observations,
+    interactiveLegendFilters,
+    interactiveTimeRangeFilters,
+  ]);
+
+  const segmentData = useMemo(() => {
+    return observations.filter(overEvery(interactiveTimeRangeFilters));
+  }, [observations, interactiveTimeRangeFilters]);
+
+  const timeRangeData = useMemo(() => {
+    return observations.filter(overEvery(timeRangeFilters));
+  }, [observations, timeRangeFilters]);
+
+  const paddingData = useMemo(() => {
+    if (dynamicScales) {
+      return chartData;
+    } else {
+      return observations.filter(overEvery(interactiveLegendFilters));
+    }
+  }, [dynamicScales, chartData, observations, interactiveLegendFilters]);
+
+  return {
+    chartData,
+    scalesData,
+    segmentData,
+    timeRangeData,
+    paddingData,
+  };
+};
+
 // TODO: base this on UI encodings?
 export type InteractiveXTimeRangeState = {
   xScaleTimeRange: ScaleTime<number, number>;
 };
+
+export type InteractiveYTimeRangeState = {
+  yScaleTimeRange: ScaleTime<number, number>;
+};
diff --git a/app/charts/shared/imputation.tsx b/app/charts/shared/imputation.tsx
index 25bc1e71e..d1d69c3cd 100644
--- a/app/charts/shared/imputation.tsx
+++ b/app/charts/shared/imputation.tsx
@@ -88,6 +88,59 @@ export const imputeTemporalLinearSeries = ({
   return dataSortedByX as Array<TemporalSeriesAfterImputationEntry>;
 };
 
+export const imputeTemporalLinearSeriesInverted = ({
+  dataSortedByY,
+}: {
+  dataSortedByY: Array<TemporalSeriesBeforeImputationEntry>;
+}): Array<TemporalSeriesAfterImputationEntry> => {
+  const presentDataIndexes = [];
+  const missingDataIndexes = [];
+
+  for (let i = 0; i < dataSortedByY.length; i++) {
+    if (dataSortedByY[i].value !== null) {
+      presentDataIndexes.push(i);
+    } else {
+      missingDataIndexes.push(i);
+    }
+  }
+
+  for (const missingDataIndex of missingDataIndexes) {
+    const nextPresentDataIndex = presentDataIndexes.findIndex(
+      (d) => d > missingDataIndex
+    );
+
+    if (nextPresentDataIndex) {
+      const previousPresentDataIndex = nextPresentDataIndex - 1;
+
+      if (previousPresentDataIndex >= 0) {
+        const previous =
+          dataSortedByY[presentDataIndexes[previousPresentDataIndex]];
+        const next = dataSortedByY[presentDataIndexes[nextPresentDataIndex]];
+
+        dataSortedByY[missingDataIndex] = {
+          date: dataSortedByY[missingDataIndex].date,
+          value: interpolateTemporalLinearValue({
+            previousValue: previous.value!,
+            nextValue: next.value!,
+            previousTime: previous.date.getTime(),
+            nextTime: next.date.getTime(),
+            currentTime: dataSortedByY[missingDataIndex].date.getTime(),
+          }),
+        };
+
+        continue;
+      }
+    }
+
+    dataSortedByY[missingDataIndex] = {
+      date: dataSortedByY[missingDataIndex].date,
+      value: 0,
+    };
+  }
+
+  return dataSortedByY as Array<TemporalSeriesAfterImputationEntry>;
+};
+
 export const isUsingImputation = (chartConfig: ChartConfig): boolean => {
   if (isAreaConfig(chartConfig)) {
     const imputationType = chartConfig.fields.y.imputationType || "";
diff --git a/app/charts/shared/interaction/tooltip-content.tsx b/app/charts/shared/interaction/tooltip-content.tsx
index dd4fc3c89..2f8f92b4d 100644
--- a/app/charts/shared/interaction/tooltip-content.tsx
+++ b/app/charts/shared/interaction/tooltip-content.tsx
@@ -41,6 +41,43 @@ export const TooltipSingle = ({
   );
 };
 
+export const TooltipSingleInverted = ({
+  yValue,
+  segment,
+  xValue,
+  xError,
+}: {
+  yValue?: string;
+  segment?: string;
+  xValue?: string;
+  xError?: string;
+}) => {
+  return (
+    <Box>
+      {yValue && (
+        <Typography
+          component="div"
+          variant="caption"
+          sx={{ fontWeight: "bold" }}
+        >
+          {yValue}
+        </Typography>
+      )}
+      {segment && (
+        <Typography component="div" variant="caption">
+          {segment}
+        </Typography>
+      )}
+      {xValue && (
+        <Typography component="div" variant="caption">
+          {xValue}
+          {xError ? <> ± {xError}</> : null}
+        </Typography>
+      )}
+    </Box>
+  );
+};
+
 export const TooltipMultiple = ({
   xValue,
   segmentValues,
@@ -72,6 +109,37 @@ export const TooltipMultiple = ({
   );
 };
 
+export const TooltipMultipleInverted = ({
+  yValue,
+  segmentValues,
+}: {
+  yValue?: string;
+  segmentValues: TooltipValue[];
+}) => {
+  return (
+    <Box>
+      {yValue && (
+        <Typography
+          component="div"
+          variant="caption"
+          sx={{ fontWeight: "bold" }}
+        >
+          {yValue}
+        </Typography>
+      )}
+      {segmentValues.map((d, i) => (
+        <LegendItem
+          key={i}
+          item={`${d.label}: ${d.value}${d.error ? ` ± ${d.error}` : ""}`}
+          color={d.color}
+          symbol={d.symbol ?? "square"}
+          usage="tooltip"
+        />
+      ))}
+    </Box>
+  );
+};
+
 // Chart Specific
 export const TooltipScatterplot = ({
   firstLine,
diff --git a/app/charts/shared/interaction/tooltip.tsx b/app/charts/shared/interaction/tooltip.tsx
index 3f6a94ac9..e3625de79 100644
--- a/app/charts/shared/interaction/tooltip.tsx
+++ b/app/charts/shared/interaction/tooltip.tsx
@@ -1,5 +1,6 @@
 import { ReactNode } from "react";
 
+import { BarsState } from "@/charts/bar/bars-state";
 import { LinesState } from "@/charts/line/lines-state";
 import { PieState } from "@/charts/pie/pie-state";
 import { useChartState } from "@/charts/shared/chart-state";
@@ -9,17 +10,35 @@ import {
 } from "@/charts/shared/interaction/tooltip-box";
 import {
   TooltipMultiple,
+  TooltipMultipleInverted,
   TooltipSingle,
+  TooltipSingleInverted,
 } from "@/charts/shared/interaction/tooltip-content";
 import { LegendSymbol } from "@/charts/shared/legend-color";
 import { useInteraction } from "@/charts/shared/use-interaction";
 import { Observation } from "@/domain/data";
 
-export const Tooltip = ({ type = "single" }: { type: TooltipType }) => {
+export const Tooltip = ({
+  type = "single",
+  inverted = false,
+}: {
+  type: TooltipType;
+  inverted?: boolean;
+}) => {
   const [state] = useInteraction();
   const { visible, d } = state.interaction;
 
-  return <>{visible && d && <TooltipInner d={d} type={type} />}</>;
+  return (
+    <>
+      {visible &&
+        d &&
+        (inverted ? (
+          <TooltipInnerInverted d={d} type={type} />
+        ) : (
+          <TooltipInner d={d} type={type} />
+        ))}
+    </>
+  );
 };
 export type { TooltipPlacement };
 
@@ -33,6 +52,7 @@ export interface TooltipValue {
   yPos?: number;
   symbol?: LegendSymbol;
 }
+
 export interface TooltipInfo {
   xAnchor: number;
   yAnchor: number | undefined;
@@ -43,6 +63,16 @@ export interface TooltipInfo {
   values: TooltipValue[] | undefined;
 }
 
+export interface TooltipInfoInverted {
+  xAnchor: number;
+  yAnchor: number | undefined;
+  placement: TooltipPlacement;
+  yValue: string;
+  tooltipContent?: ReactNode;
+  datum: TooltipValue;
+  values: TooltipValue[] | undefined;
+}
+
 const TooltipInner = ({ d, type }: { d: Observation; type: TooltipType }) => {
   const { bounds, getAnnotationInfo } = useChartState() as
     | LinesState
@@ -72,3 +102,37 @@ const TooltipInner = ({ d, type }: { d: Observation; type: TooltipType }) => {
     </TooltipBox>
   );
 };
+
+const TooltipInnerInverted = ({
+  d,
+  type,
+}: {
+  d: Observation;
+  type: TooltipType;
+}) => {
+  const { bounds, getAnnotationInfo } = useChartState() as BarsState;
+  const { margins } = bounds;
+  const { xAnchor, yAnchor, placement, yValue, tooltipContent, datum, values } =
+    getAnnotationInfo(d as any);
+
+  if (Number.isNaN(yAnchor)) {
+    return null;
+  }
+
+  return (
+    <TooltipBox x={xAnchor} y={yAnchor} placement={placement} margins={margins}>
+      {tooltipContent ? (
+        tooltipContent
+      ) : type === "multiple" && values ? (
+        <TooltipMultipleInverted yValue={yValue} segmentValues={values} />
+      ) : (
+        <TooltipSingleInverted
+          yValue={yValue}
+          segment={datum.label}
+          xValue={datum.value}
+          xError={datum.error}
+        />
+      )}
+    </TooltipBox>
+  );
+};
diff --git a/app/charts/shared/rendering-utils.ts b/app/charts/shared/rendering-utils.ts
index 38970bd8b..3725b89eb 100644
--- a/app/charts/shared/rendering-utils.ts
+++ b/app/charts/shared/rendering-utils.ts
@@ -163,6 +163,16 @@ export type RenderWhiskerDatum = {
   renderMiddleCircle?: boolean;
 };
 
+export type RenderWhiskerBarDatum = {
+  key: string;
+  y: number;
+  x1: number;
+  x2: number;
+  width: number;
+  fill?: string;
+  renderMiddleCircle?: boolean;
+};
+
 export const renderWhiskers = (
   g: Selection<SVGGElement, null, SVGGElement, unknown>,
   data: RenderWhiskerDatum[],
@@ -275,6 +285,118 @@ export const renderWhiskers = (
     );
 };
 
+export const renderBarWhiskers = (
+  g: Selection<SVGGElement, null, SVGGElement, unknown>,
+  data: RenderWhiskerBarDatum[],
+  options: RenderOptions
+) => {
+  const { transition } = options;
+
+  g.selectAll<SVGGElement, RenderWhiskerDatum>("g")
+    .data(data, (d) => d.key)
+    .join(
+      (enter) =>
+        enter
+          .append("g")
+          .attr("opacity", 0)
+          .call((g) =>
+            g
+              .append("rect")
+              .attr("class", "top")
+              .attr("y", (d) => d.y)
+              .attr("x", (d) => d.x2)
+              .attr("width", (d) => d.width)
+              .attr("height", ERROR_WHISKER_SIZE)
+              .attr("fill", (d) => d.fill ?? "black")
+              .attr("stroke", "none")
+          )
+          .call((g) =>
+            g
+              .append("rect")
+              .attr("class", "middle")
+              .attr("y", (d) => d.y + (d.width - ERROR_WHISKER_SIZE) / 2)
+              .attr("x", (d) => d.x2)
+              .attr("width", ERROR_WHISKER_SIZE)
+              .attr("height", (d) => Math.max(0, d.x1 - d.x2))
+              .attr("fill", (d) => d.fill ?? "black")
+              .attr("stroke", "none")
+          )
+          .call((g) =>
+            g
+              .append("rect")
+              .attr("class", "bottom")
+              .attr("y", (d) => d.y)
+              .attr("x", (d) => d.x1)
+              .attr("width", (d) => d.width)
+              .attr("height", ERROR_WHISKER_SIZE)
+              .attr("fill", (d) => d.fill ?? "black")
+              .attr("stroke", "none")
+          )
+          .call((g) =>
+            g
+              .filter((d) => d.renderMiddleCircle ?? false)
+              .append("circle")
+              .attr("class", "middle-circle")
+              .attr("cy", (d) => d.y + d.width / 2)
+              .attr("cx", (d) => (d.x1 + d.x2) / 2)
+              .attr("r", ERROR_WHISKER_MIDDLE_CIRCLE_RADIUS)
+              .attr("fill", (d) => d.fill ?? "black")
+              .attr("stroke", "none")
+          )
+          .call((enter) =>
+            maybeTransition(enter, {
+              s: (g) => g.attr("opacity", 1),
+              transition,
+            })
+          ),
+      (update) =>
+        maybeTransition(update, {
+          s: (g) =>
+            g
+              .attr("opacity", 1)
+              .call((g) =>
+                g
+                  .select(".top")
+                  .attr("y", (d) => d.y)
+                  .attr("x", (d) => d.x2)
+                  .attr("width", (d) => d.width)
+                  .attr("fill", (d) => d.fill ?? "black")
+              )
+              .call((g) =>
+                g
+                  .select(".middle")
+                  .attr("y", (d) => d.y + (d.width - ERROR_WHISKER_SIZE) / 2)
+                  .attr("x", (d) => d.x2)
+                  .attr("height", (d) => Math.max(0, d.x1 - d.x2))
+                  .attr("fill", (d) => d.fill ?? "black")
+              )
+              .call((g) =>
+                g
+                  .select(".bottom")
+                  .attr("y", (d) => d.y)
+                  .attr("x", (d) => d.x1)
+                  .attr("width", (d) => d.width)
+                  .attr("fill", (d) => d.fill ?? "black")
+              )
+              .call((g) =>
+                g
+                  .select(".middle-circle")
+                  .attr("cy", (d) => d.y + d.width / 2)
+                  .attr("cx", (d) => (d.x1 + d.x2) / 2)
+                  .attr("r", ERROR_WHISKER_MIDDLE_CIRCLE_RADIUS)
+                  .attr("fill", (d) => d.fill ?? "black")
+                  .attr("stroke", "none")
+              ),
+          transition,
+        }),
+      (exit) =>
+        maybeTransition(exit, {
+          transition,
+          s: (g) => g.attr("opacity", 0).remove(),
+        })
+    );
+};
+
 export const filterWithoutErrors = (
   getError: ((d: Observation) => ObservationValue) | null
 ) => {
diff --git a/app/charts/shared/stacked-helpers.ts b/app/charts/shared/stacked-helpers.ts
index 6f2bb7795..11c11a807 100644
--- a/app/charts/shared/stacked-helpers.ts
+++ b/app/charts/shared/stacked-helpers.ts
@@ -9,6 +9,7 @@ import { NumericalMeasure, Observation } from "@/domain/data";
 import { formatNumberWithUnit } from "@/formatters";
 
 const NORMALIZED_Y_DOMAIN = [0, 100];
+const NORMALIZED_X_DOMAIN = [0, 100];
 
 export const getStackedYScale = (
   data: Observation[],
@@ -49,6 +50,45 @@ export const getStackedYScale = (
   return yScale;
 };
 
+export const getStackedXScale = (
+  data: Observation[],
+  options: {
+    normalize: boolean;
+    getY: StringValueGetter;
+    getX: NumericalValueGetter;
+    getTime?: StringValueGetter;
+  }
+): ScaleLinear<number, number> => {
+  const { normalize, getX, getY, getTime } = options;
+  const xScale = scaleLinear();
+
+  if (normalize) {
+    xScale.domain(NORMALIZED_X_DOMAIN);
+  } else {
+    const grouped = group(data, (d) => getY(d) + getTime?.(d));
+    let xMin = 0;
+    let xMax = 0;
+
+    for (const [, v] of grouped) {
+      const values = v.map(getX).filter((d) => d !== null) as number[];
+      const newXMin = sum(values.filter((d) => d < 0));
+      const newXMax = sum(values.filter((d) => d >= 0));
+
+      if (xMin === undefined || newXMin < xMin) {
+        xMin = newXMin;
+      }
+
+      if (xMax === undefined || newXMax > xMax) {
+        xMax = newXMax;
+      }
+    }
+
+    xScale.domain([xMin, xMax]).nice();
+  }
+
+  return xScale;
+};
+
 export const getStackedTooltipValueFormatter = ({
   normalize,
   yMeasureId,
@@ -79,3 +119,34 @@ export const getStackedTooltipValueFormatter = ({
     return formatNumberWithUnit(d, format, yMeasureUnit);
   };
 };
+
+export const getStackedTooltipValueFormatterInverted = ({
+  normalize,
+  xMeasureId,
+  xMeasureUnit,
+  formatters,
+  formatNumber,
+}: {
+  normalize: boolean;
+  xMeasureId: string;
+  xMeasureUnit: NumericalMeasure["unit"];
+  formatters: { [k: string]: (s: any) => string };
+  formatNumber: (d: NumberValue | null | undefined) => string;
+}) => {
+  return (d: number | null, dIdentity: number | null) => {
+    if (d === null && dIdentity === null) {
+      return "-";
+    }
+
+    const format = formatters[xMeasureId] ?? formatNumber;
+
+    if (normalize) {
+      const rounded = Math.round(d as number);
+      const fValue = formatNumberWithUnit(dIdentity, format, xMeasureUnit);
+
+      return `${rounded}% (${fValue})`;
+    }
+
+    return formatNumberWithUnit(d, format, xMeasureUnit);
+  };
+};
diff --git a/app/config-types.ts b/app/config-types.ts
index 635a77e40..7ab27cd18 100644
--- a/app/config-types.ts
+++ b/app/config-types.ts
@@ -316,8 +316,8 @@ export type BarSegmentField = t.TypeOf<typeof BarSegmentField>;
 
 const BarFields = t.intersection([
   t.type({
-    x: t.intersection([GenericField, SortingField]),
-    y: GenericField,
+    x: GenericField,
+    y: t.intersection([GenericField, SortingField]),
   }),
   t.partial({
     segment: BarSegmentField,

From fffca2f5189a22c53601101aea907344123d0530 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Fri, 29 Nov 2024 13:47:06 +0000
Subject: [PATCH 04/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20remove=20rot?=
 =?UTF-8?q?ation=20from=20left=20axis?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/axis-width-band-vertical.tsx | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/app/charts/shared/axis-width-band-vertical.tsx b/app/charts/shared/axis-width-band-vertical.tsx
index b8a3d32b0..c5e7a1989 100644
--- a/app/charts/shared/axis-width-band-vertical.tsx
+++ b/app/charts/shared/axis-width-band-vertical.tsx
@@ -24,14 +24,13 @@ export const AxisWidthBand = () => {
 
   useEffect(() => {
     if (ref.current) {
-      const rotation = true;
       const hasNegativeValues = xScale.domain()[0] < 0;
       const fontSize =
         yScale.bandwidth() > labelFontSize ? labelFontSize : yScale.bandwidth();
       const axis = axisLeft(yScale)
         .tickSizeOuter(0)
-        .tickSizeInner(hasNegativeValues ? -chartHeight : 6)
-        .tickPadding(rotation ? -10 : 0);
+        .tickSizeInner(hasNegativeValues ? -chartHeight : 6);
+      // .tickPadding(rotation ? -10 : 0);
 
       if (yTimeUnit) {
         axis.tickFormat((d) => formatDate(d, yTimeUnit));
@@ -57,12 +56,11 @@ export const AxisWidthBand = () => {
         hasNegativeValues ? gridColor : domainColor
       );
       g.selectAll(".tick text")
-        .attr("transform", rotation ? "rotate(90)" : "rotate(0)")
-        .attr("x", rotation ? fontSize : 0)
+        .attr("x", 0)
         .attr("font-size", fontSize)
         .attr("font-family", fontFamily)
         .attr("fill", labelColor)
-        .attr("text-anchor", rotation ? "start" : "unset");
+        .attr("text-anchor", "unset");
     }
   }, [
     chartHeight,

From 4d791db8034c7d7c1141bfec136ec0c4078da7d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Fri, 29 Nov 2024 13:47:37 +0000
Subject: [PATCH 05/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20adjust=20lef?=
 =?UTF-8?q?t=20margin?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-grouped-state.tsx | 11 ++---------
 app/charts/bar/bars-stacked-state.tsx | 11 ++---------
 app/charts/bar/bars-state.tsx         | 12 +++---------
 3 files changed, 7 insertions(+), 27 deletions(-)

diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx
index 71716c822..1fb2937f7 100644
--- a/app/charts/bar/bars-grouped-state.tsx
+++ b/app/charts/bar/bars-grouped-state.tsx
@@ -23,7 +23,6 @@ import {
   PADDING_WITHIN,
 } from "@/charts/bar/constants";
 import {
-  useAxisLabelHeightOffset,
   useChartBounds,
   useChartPadding,
 } from "@/charts/shared/chart-dimensions";
@@ -341,17 +340,11 @@ const useBarsGroupedState = (
       : yTimeRangeDomainLabels,
   });
   const right = 40;
-  const { offset: yAxisLabelMargin } = useAxisLabelHeightOffset({
-    label: xMeasure.label,
-    width,
-    marginLeft: left,
-    marginRight: right,
-  });
   const margins = {
-    top: 50 + yAxisLabelMargin,
+    top: 0,
     right,
     bottom,
-    left,
+    left: 50 + left,
   };
   const bounds = useChartBounds(width, margins, height);
   const { chartWidth, chartHeight } = bounds;
diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index 76f892174..1e9aefe9b 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -27,7 +27,6 @@ import {
 } from "@/charts/bar/bars-stacked-state-props";
 import { PADDING_INNER, PADDING_OUTER } from "@/charts/bar/constants";
 import {
-  useAxisLabelHeightOffset,
   useChartBounds,
   useChartPadding,
 } from "@/charts/shared/chart-dimensions";
@@ -401,17 +400,11 @@ const useBarsStackedState = (
     normalize,
   });
   const right = 40;
-  const { offset: yAxisLabelMargin } = useAxisLabelHeightOffset({
-    label: xMeasure.label,
-    width,
-    marginLeft: left,
-    marginRight: right,
-  });
   const margins = {
-    top: 50 + yAxisLabelMargin,
+    top: 0,
     right,
     bottom,
-    left,
+    left: 50 + left,
   };
   const bounds = useChartBounds(width, margins, height);
   const { chartWidth, chartHeight } = bounds;
diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
index 65d27612e..8947d2691 100644
--- a/app/charts/bar/bars-state.tsx
+++ b/app/charts/bar/bars-state.tsx
@@ -16,7 +16,6 @@ import {
 } from "@/charts/bar/bars-state-props";
 import { PADDING_INNER, PADDING_OUTER } from "@/charts/bar/constants";
 import {
-  useAxisLabelHeightOffset,
   useChartBounds,
   useChartPadding,
 } from "@/charts/shared/chart-dimensions";
@@ -203,17 +202,12 @@ const useBarsState = (
       : yTimeRangeDomainLabels,
   });
   const right = 40;
-  const { offset: xAxisLabelMargin } = useAxisLabelHeightOffset({
-    label: xMeasure.label,
-    width,
-    marginLeft: left,
-    marginRight: right,
-  });
   const margins = {
-    top: 50 + xAxisLabelMargin,
+    top: 0,
     right,
     bottom,
-    left,
+    //NOTE: hardcoded for the moment
+    left: 50 + left,
   };
 
   const bounds = useChartBounds(width, margins, height);

From db39b8d78c7cbc60fc397b370cad70233a8f51c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Fri, 29 Nov 2024 14:24:01 +0000
Subject: [PATCH 06/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20adjust=20interactio?=
 =?UTF-8?q?n=20scale?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-state.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
index 8947d2691..54c73ef4a 100644
--- a/app/charts/bar/bars-state.tsx
+++ b/app/charts/bar/bars-state.tsx
@@ -130,7 +130,8 @@ const useBarsState = (
       .paddingInner(PADDING_INNER)
       .paddingOuter(PADDING_OUTER);
     const yScaleInteraction = scaleBand()
-      .domain(bandDomain)
+      //NOTE: not sure if this is the right way to go here
+      .domain(bandDomain.reverse())
       .paddingInner(0)
       .paddingOuter(0);
 

From e63474e129d4ac24262013f699cdbda7146c8f8a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Fri, 29 Nov 2024 15:12:54 +0000
Subject: [PATCH 07/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20x=20scale=20on=20gr?=
 =?UTF-8?q?ouped=20bar=20charts?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-grouped-state.tsx | 2 +-
 app/charts/bar/bars-grouped.tsx       | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx
index 1fb2937f7..b2c66b679 100644
--- a/app/charts/bar/bars-grouped-state.tsx
+++ b/app/charts/bar/bars-grouped-state.tsx
@@ -252,7 +252,7 @@ const useBarsGroupedState = (
       ) ?? 0,
       0
     );
-    const xScale = scaleLinear().domain([minValue, maxValue]).nice();
+    const xScale = scaleLinear().domain([maxValue, minValue]).nice();
 
     const minPaddingValue = getMinX(paddingData, (d) =>
       getXErrorRange ? getXErrorRange(d)[0] : getX(d)
diff --git a/app/charts/bar/bars-grouped.tsx b/app/charts/bar/bars-grouped.tsx
index 3b9d9c18a..87d54e2de 100644
--- a/app/charts/bar/bars-grouped.tsx
+++ b/app/charts/bar/bars-grouped.tsx
@@ -111,7 +111,7 @@ export const BarsGrouped = () => {
         return {
           key,
           y: (yScale(segment) as number) + (yScaleIn(y) as number),
-          x: xScale(Math.max(x, 0)),
+          x: xScale(Math.min(x, 0)),
           width: Math.max(0, Math.abs(xScale(x) - x0)),
           height: bandwidth,
           color: colors(y),

From d437b9c9e8c5a8bba126cfb0cf75e9324fd62305 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Fri, 29 Nov 2024 15:37:10 +0000
Subject: [PATCH 08/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20adjust=20grouped/st?=
 =?UTF-8?q?acked=20chart=20width=20and=20height?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-grouped-state.tsx | 8 ++++----
 app/charts/bar/bars-stacked-state.tsx | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx
index b2c66b679..db274ee68 100644
--- a/app/charts/bar/bars-grouped-state.tsx
+++ b/app/charts/bar/bars-grouped-state.tsx
@@ -350,11 +350,11 @@ const useBarsGroupedState = (
   const { chartWidth, chartHeight } = bounds;
 
   // Adjust of scales based on chart dimensions
-  yScale.range([0, chartWidth]);
-  yScaleInteraction.range([0, chartWidth]);
+  yScale.range([0, chartHeight]);
+  yScaleInteraction.range([0, chartHeight]);
   yScaleIn.range([0, yScale.bandwidth()]);
-  yScaleTimeRange.range([0, chartWidth]);
-  xScale.range([chartHeight, 0]);
+  yScaleTimeRange.range([0, chartHeight]);
+  xScale.range([chartWidth, 0]);
 
   const isMobile = useIsMobile();
 
diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index 1e9aefe9b..d31c50f2f 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -409,10 +409,10 @@ const useBarsStackedState = (
   const bounds = useChartBounds(width, margins, height);
   const { chartWidth, chartHeight } = bounds;
 
-  yScale.range([0, chartWidth]);
-  yScaleInteraction.range([0, chartWidth]);
-  yScaleTimeRange.range([0, chartWidth]);
-  xScale.range([chartHeight, 0]);
+  yScale.range([0, chartHeight]);
+  yScaleInteraction.range([0, chartHeight]);
+  yScaleTimeRange.range([0, chartHeight]);
+  xScale.range([chartWidth, 0]);
 
   const isMobile = useIsMobile();
 

From 4f4165c7bcca5ab192cb2879db70ebbeb961d95f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Mon, 2 Dec 2024 14:20:27 +0000
Subject: [PATCH 09/54] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20addresse?=
 =?UTF-8?q?d=20some=20of=20the=20feedback?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-grouped-state-props.ts    |  4 +-
 app/charts/bar/bars-grouped-state.tsx         |  2 +-
 app/charts/bar/bars-grouped.tsx               | 10 ++---
 app/charts/bar/bars.tsx                       | 10 ++---
 app/charts/bar/chart-bar.tsx                  | 18 ++++----
 app/charts/column/columns-grouped-state.tsx   |  2 +-
 app/charts/column/columns-grouped.tsx         | 10 ++---
 app/charts/column/columns.tsx                 | 10 ++---
 app/charts/line/lines.tsx                     | 10 ++---
 ...band-vertical.tsx => axis-height-band.tsx} |  7 ++--
 app/charts/shared/chart-state.ts              |  1 -
 app/charts/shared/rendering-utils.ts          | 16 ++++----
 app/charts/shared/stacked-helpers.ts          |  7 ++--
 app/config-types.ts                           | 41 ++++++-------------
 14 files changed, 65 insertions(+), 83 deletions(-)
 rename app/charts/shared/{axis-width-band-vertical.tsx => axis-height-band.tsx} (95%)

diff --git a/app/charts/bar/bars-grouped-state-props.ts b/app/charts/bar/bars-grouped-state-props.ts
index 4877d4fed..f81efdae3 100644
--- a/app/charts/bar/bars-grouped-state-props.ts
+++ b/app/charts/bar/bars-grouped-state-props.ts
@@ -61,7 +61,7 @@ export const useBarsGroupedStateVariables = (
     dimensionsById,
     observations,
   });
-  const numericalYErrorVariables = useNumericalXErrorVariables(x, {
+  const numericalXErrorVariables = useNumericalXErrorVariables(x, {
     numericalXVariables,
     dimensions,
     measures,
@@ -116,7 +116,7 @@ export const useBarsGroupedStateVariables = (
     sortData,
     ...bandYVariables,
     ...numericalXVariables,
-    ...numericalYErrorVariables,
+    ...numericalXErrorVariables,
     ...segmentVariables,
     ...interactiveFiltersVariables,
     getRenderingKey,
diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx
index db274ee68..41b8ea48d 100644
--- a/app/charts/bar/bars-grouped-state.tsx
+++ b/app/charts/bar/bars-grouped-state.tsx
@@ -172,7 +172,7 @@ const useBarsGroupedState = (
     return Object.fromEntries(
       rollup(
         chartData,
-        (v) => sum(v, (y) => getX(y)),
+        (v) => sum(v, (d) => getX(d)),
         (y) => getY(y)
       )
     );
diff --git a/app/charts/bar/bars-grouped.tsx b/app/charts/bar/bars-grouped.tsx
index 87d54e2de..57f8cf2dd 100644
--- a/app/charts/bar/bars-grouped.tsx
+++ b/app/charts/bar/bars-grouped.tsx
@@ -5,9 +5,9 @@ import { RenderBarDatum, renderBars } from "@/charts/bar/rendering-utils";
 import { useChartState } from "@/charts/shared/chart-state";
 import {
   filterWithoutErrors,
-  renderBarWhiskers,
+  renderHorizontalWhisker,
   renderContainer,
-  RenderWhiskerBarDatum,
+  RenderHorizontalWhiskerDatum,
 } from "@/charts/shared/rendering-utils";
 import { useTransitionStore } from "@/stores/transition";
 
@@ -27,7 +27,7 @@ export const ErrorWhiskers = () => {
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
-  const renderData: RenderWhiskerBarDatum[] = useMemo(() => {
+  const renderData: RenderHorizontalWhiskerDatum[] = useMemo(() => {
     if (!getXErrorRange || !showXStandardError) {
       return [];
     }
@@ -46,7 +46,7 @@ export const ErrorWhiskers = () => {
             x1: xScale(x1),
             x2: xScale(x2),
             width: barWidth,
-          } as RenderWhiskerBarDatum;
+          } as RenderHorizontalWhiskerDatum;
         })
       );
     // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -69,7 +69,7 @@ export const ErrorWhiskers = () => {
         id: "bars-grouped-error-whiskers",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g, opts) => renderBarWhiskers(g, renderData, opts),
+        render: (g, opts) => renderHorizontalWhisker(g, renderData, opts),
       });
     }
   }, [
diff --git a/app/charts/bar/bars.tsx b/app/charts/bar/bars.tsx
index e7099e1d2..55e3aa914 100644
--- a/app/charts/bar/bars.tsx
+++ b/app/charts/bar/bars.tsx
@@ -5,10 +5,10 @@ import { BarsState } from "@/charts/bar/bars-state";
 import { RenderBarDatum, renderBars } from "@/charts/bar/rendering-utils";
 import { useChartState } from "@/charts/shared/chart-state";
 import {
-  RenderWhiskerBarDatum,
+  RenderHorizontalWhiskerDatum,
   filterWithoutErrors,
-  renderBarWhiskers,
   renderContainer,
+  renderHorizontalWhisker,
 } from "@/charts/shared/rendering-utils";
 import { useTransitionStore } from "@/stores/transition";
 import { useTheme } from "@/themes";
@@ -28,7 +28,7 @@ export const ErrorWhiskers = () => {
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
-  const renderData: RenderWhiskerBarDatum[] = useMemo(() => {
+  const renderData: RenderHorizontalWhiskerDatum[] = useMemo(() => {
     if (!getXErrorRange || !showXStandardError) {
       return [];
     }
@@ -44,7 +44,7 @@ export const ErrorWhiskers = () => {
         x1: xScale(x1),
         x2: xScale(x2),
         width: barWidth,
-      } as RenderWhiskerBarDatum;
+      } as RenderHorizontalWhiskerDatum;
     });
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [
@@ -65,7 +65,7 @@ export const ErrorWhiskers = () => {
         id: "bars-error-whiskers",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g, opts) => renderBarWhiskers(g, renderData, opts),
+        render: (g, opts) => renderHorizontalWhisker(g, renderData, opts),
       });
     }
   }, [
diff --git a/app/charts/bar/chart-bar.tsx b/app/charts/bar/chart-bar.tsx
index 05ccdd948..d4d791577 100644
--- a/app/charts/bar/chart-bar.tsx
+++ b/app/charts/bar/chart-bar.tsx
@@ -12,9 +12,9 @@ import { BarChart } from "@/charts/bar/bars-state";
 import { InteractionBars } from "@/charts/bar/overlay-bars";
 import { ChartDataWrapper } from "@/charts/chart-data-wrapper";
 import {
-  AxisWidthBand,
-  AxisWidthBandDomain,
-} from "@/charts/shared/axis-width-band-vertical";
+  AxisHeightBand,
+  AxisHeightBandDomain,
+} from "@/charts/shared/axis-height-band";
 import { AxisWidthLinear } from "@/charts/shared/axis-width-linear";
 import { BrushTime, shouldShowBrush } from "@/charts/shared/brush";
 import {
@@ -54,9 +54,9 @@ const ChartBars = memo((props: ChartProps<BarConfig>) => {
           <ChartContainer>
             <ChartSvg>
               <AxisWidthLinear />
-              <AxisWidthBand />
+              <AxisHeightBand />
               <BarsStacked />
-              <AxisWidthBandDomain />
+              <AxisHeightBandDomain />
               <InteractionBars />
               {showTimeBrush && <BrushTime />}
             </ChartSvg>
@@ -84,10 +84,10 @@ const ChartBars = memo((props: ChartProps<BarConfig>) => {
           <ChartContainer>
             <ChartSvg>
               <AxisWidthLinear />
-              <AxisWidthBand />
+              <AxisHeightBand />
               <BarsGrouped />
               <ErrorWhiskersGrouped />
-              <AxisWidthBandDomain />
+              <AxisHeightBandDomain />
               <InteractionBars />
               {showTimeBrush && <BrushTime />}
             </ChartSvg>
@@ -115,10 +115,10 @@ const ChartBars = memo((props: ChartProps<BarConfig>) => {
           <ChartContainer>
             <ChartSvg>
               <AxisWidthLinear />
-              <AxisWidthBand />
+              <AxisHeightBand />
               <Bars />
               <ErrorWhiskers />
-              <AxisWidthBandDomain />
+              <AxisHeightBandDomain />
               <InteractionBars />
               {showTimeBrush && <BrushTime />}
             </ChartSvg>
diff --git a/app/charts/column/columns-grouped-state.tsx b/app/charts/column/columns-grouped-state.tsx
index dc6539d94..4bf79220e 100644
--- a/app/charts/column/columns-grouped-state.tsx
+++ b/app/charts/column/columns-grouped-state.tsx
@@ -173,7 +173,7 @@ const useColumnsGroupedState = (
     return Object.fromEntries(
       rollup(
         chartData,
-        (v) => sum(v, (x) => getY(x)),
+        (v) => sum(v, (d) => getY(d)),
         (x) => getX(x)
       )
     );
diff --git a/app/charts/column/columns-grouped.tsx b/app/charts/column/columns-grouped.tsx
index a3b87609d..627dd75ed 100644
--- a/app/charts/column/columns-grouped.tsx
+++ b/app/charts/column/columns-grouped.tsx
@@ -7,10 +7,10 @@ import {
 } from "@/charts/column/rendering-utils";
 import { useChartState } from "@/charts/shared/chart-state";
 import {
-  RenderWhiskerDatum,
+  RenderVerticalWhiskerDatum,
   filterWithoutErrors,
   renderContainer,
-  renderWhiskers,
+  renderVerticalWhiskers,
 } from "@/charts/shared/rendering-utils";
 import { useTransitionStore } from "@/stores/transition";
 
@@ -30,7 +30,7 @@ export const ErrorWhiskers = () => {
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
-  const renderData: RenderWhiskerDatum[] = useMemo(() => {
+  const renderData: RenderVerticalWhiskerDatum[] = useMemo(() => {
     if (!getYErrorRange || !showYStandardError) {
       return [];
     }
@@ -49,7 +49,7 @@ export const ErrorWhiskers = () => {
             y1: yScale(y1),
             y2: yScale(y2),
             width: barWidth,
-          } as RenderWhiskerDatum;
+          } as RenderVerticalWhiskerDatum;
         })
       );
     // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -72,7 +72,7 @@ export const ErrorWhiskers = () => {
         id: "columns-grouped-error-whiskers",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g, opts) => renderWhiskers(g, renderData, opts),
+        render: (g, opts) => renderVerticalWhiskers(g, renderData, opts),
       });
     }
   }, [
diff --git a/app/charts/column/columns.tsx b/app/charts/column/columns.tsx
index cfcc9a256..a71ea0897 100644
--- a/app/charts/column/columns.tsx
+++ b/app/charts/column/columns.tsx
@@ -8,10 +8,10 @@ import {
 } from "@/charts/column/rendering-utils";
 import { useChartState } from "@/charts/shared/chart-state";
 import {
-  RenderWhiskerDatum,
+  RenderVerticalWhiskerDatum,
   filterWithoutErrors,
   renderContainer,
-  renderWhiskers,
+  renderVerticalWhiskers,
 } from "@/charts/shared/rendering-utils";
 import { useTransitionStore } from "@/stores/transition";
 import { useTheme } from "@/themes";
@@ -31,7 +31,7 @@ export const ErrorWhiskers = () => {
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
-  const renderData: RenderWhiskerDatum[] = useMemo(() => {
+  const renderData: RenderVerticalWhiskerDatum[] = useMemo(() => {
     if (!getYErrorRange || !showYStandardError) {
       return [];
     }
@@ -47,7 +47,7 @@ export const ErrorWhiskers = () => {
         y1: yScale(y1),
         y2: yScale(y2),
         width: barWidth,
-      } as RenderWhiskerDatum;
+      } as RenderVerticalWhiskerDatum;
     });
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [
@@ -68,7 +68,7 @@ export const ErrorWhiskers = () => {
         id: "columns-error-whiskers",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g, opts) => renderWhiskers(g, renderData, opts),
+        render: (g, opts) => renderVerticalWhiskers(g, renderData, opts),
       });
     }
   }, [
diff --git a/app/charts/line/lines.tsx b/app/charts/line/lines.tsx
index c0a63bd1f..1e1f8c2e1 100644
--- a/app/charts/line/lines.tsx
+++ b/app/charts/line/lines.tsx
@@ -4,10 +4,10 @@ import { Fragment, memo, useEffect, useMemo, useRef } from "react";
 import { LinesState } from "@/charts/line/lines-state";
 import { useChartState } from "@/charts/shared/chart-state";
 import {
-  RenderWhiskerDatum,
+  RenderVerticalWhiskerDatum,
   filterWithoutErrors,
   renderContainer,
-  renderWhiskers,
+  renderVerticalWhiskers,
 } from "@/charts/shared/rendering-utils";
 import { Observation } from "@/domain/data";
 import { useTransitionStore } from "@/stores/transition";
@@ -29,7 +29,7 @@ export const ErrorWhiskers = () => {
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
-  const renderData: RenderWhiskerDatum[] = useMemo(() => {
+  const renderData: RenderVerticalWhiskerDatum[] = useMemo(() => {
     if (!getYErrorRange || !showYStandardError) {
       return [];
     }
@@ -47,7 +47,7 @@ export const ErrorWhiskers = () => {
         width: barWidth,
         fill: colors(segment),
         renderMiddleCircle: true,
-      } as RenderWhiskerDatum;
+      } as RenderVerticalWhiskerDatum;
     });
   }, [
     chartData,
@@ -67,7 +67,7 @@ export const ErrorWhiskers = () => {
         id: "lines-error-whiskers",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g, opts) => renderWhiskers(g, renderData, opts),
+        render: (g, opts) => renderVerticalWhiskers(g, renderData, opts),
       });
     }
   }, [
diff --git a/app/charts/shared/axis-width-band-vertical.tsx b/app/charts/shared/axis-height-band.tsx
similarity index 95%
rename from app/charts/shared/axis-width-band-vertical.tsx
rename to app/charts/shared/axis-height-band.tsx
index c5e7a1989..2d7790dc2 100644
--- a/app/charts/shared/axis-width-band-vertical.tsx
+++ b/app/charts/shared/axis-height-band.tsx
@@ -11,7 +11,7 @@ import { useChartTheme } from "@/charts/shared/use-chart-theme";
 import { useTimeFormatUnit } from "@/formatters";
 import { useTransitionStore } from "@/stores/transition";
 
-export const AxisWidthBand = () => {
+export const AxisHeightBand = () => {
   const ref = useRef<SVGGElement>(null);
   const state = useChartState() as BarsState;
   const { xScale, getYLabel, yTimeUnit, yScale, bounds } = state;
@@ -30,7 +30,6 @@ export const AxisWidthBand = () => {
       const axis = axisLeft(yScale)
         .tickSizeOuter(0)
         .tickSizeInner(hasNegativeValues ? -chartHeight : 6);
-      // .tickPadding(rotation ? -10 : 0);
 
       if (yTimeUnit) {
         axis.tickFormat((d) => formatDate(d, yTimeUnit));
@@ -83,7 +82,7 @@ export const AxisWidthBand = () => {
   return <g ref={ref} />;
 };
 
-export const AxisWidthBandDomain = () => {
+export const AxisHeightBandDomain = () => {
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
@@ -95,7 +94,7 @@ export const AxisWidthBandDomain = () => {
     if (ref.current) {
       const axis = axisLeft(yScale).tickSizeOuter(0);
       const g = renderContainer(ref.current, {
-        id: "axis-width-band-vertical-domain",
+        id: "axis-height-band-domain",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
         render: (g) => g.call(axis),
diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts
index 2d01838ef..528950ed3 100644
--- a/app/charts/shared/chart-state.ts
+++ b/app/charts/shared/chart-state.ts
@@ -109,7 +109,6 @@ export const useChartState = () => {
 export type ChartWithInteractiveXTimeRangeState =
   | AreasState
   | ColumnsState
-  // | BarsState
   | LinesState;
 
 export type NumericalValueGetter = (d: Observation) => number | null;
diff --git a/app/charts/shared/rendering-utils.ts b/app/charts/shared/rendering-utils.ts
index 3725b89eb..37aacb045 100644
--- a/app/charts/shared/rendering-utils.ts
+++ b/app/charts/shared/rendering-utils.ts
@@ -153,7 +153,7 @@ type AnyTransition = Transition<any, any, any, any>;
 const ERROR_WHISKER_SIZE = 1;
 const ERROR_WHISKER_MIDDLE_CIRCLE_RADIUS = 3.5;
 
-export type RenderWhiskerDatum = {
+export type RenderVerticalWhiskerDatum = {
   key: string;
   x: number;
   y1: number;
@@ -163,7 +163,7 @@ export type RenderWhiskerDatum = {
   renderMiddleCircle?: boolean;
 };
 
-export type RenderWhiskerBarDatum = {
+export type RenderHorizontalWhiskerDatum = {
   key: string;
   y: number;
   x1: number;
@@ -173,14 +173,14 @@ export type RenderWhiskerBarDatum = {
   renderMiddleCircle?: boolean;
 };
 
-export const renderWhiskers = (
+export const renderVerticalWhiskers = (
   g: Selection<SVGGElement, null, SVGGElement, unknown>,
-  data: RenderWhiskerDatum[],
+  data: RenderVerticalWhiskerDatum[],
   options: RenderOptions
 ) => {
   const { transition } = options;
 
-  g.selectAll<SVGGElement, RenderWhiskerDatum>("g")
+  g.selectAll<SVGGElement, RenderVerticalWhiskerDatum>("g")
     .data(data, (d) => d.key)
     .join(
       (enter) =>
@@ -285,14 +285,14 @@ export const renderWhiskers = (
     );
 };
 
-export const renderBarWhiskers = (
+export const renderHorizontalWhisker = (
   g: Selection<SVGGElement, null, SVGGElement, unknown>,
-  data: RenderWhiskerBarDatum[],
+  data: RenderHorizontalWhiskerDatum[],
   options: RenderOptions
 ) => {
   const { transition } = options;
 
-  g.selectAll<SVGGElement, RenderWhiskerDatum>("g")
+  g.selectAll<SVGGElement, RenderHorizontalWhiskerDatum>("g")
     .data(data, (d) => d.key)
     .join(
       (enter) =>
diff --git a/app/charts/shared/stacked-helpers.ts b/app/charts/shared/stacked-helpers.ts
index 11c11a807..444a8c865 100644
--- a/app/charts/shared/stacked-helpers.ts
+++ b/app/charts/shared/stacked-helpers.ts
@@ -8,8 +8,7 @@ import {
 import { NumericalMeasure, Observation } from "@/domain/data";
 import { formatNumberWithUnit } from "@/formatters";
 
-const NORMALIZED_Y_DOMAIN = [0, 100];
-const NORMALIZED_X_DOMAIN = [0, 100];
+const NORMALIZED_VALUE_DOMAIN = [0, 100];
 
 export const getStackedYScale = (
   data: Observation[],
@@ -24,7 +23,7 @@ export const getStackedYScale = (
   const yScale = scaleLinear();
 
   if (normalize) {
-    yScale.domain(NORMALIZED_Y_DOMAIN);
+    yScale.domain(NORMALIZED_VALUE_DOMAIN);
   } else {
     const grouped = group(data, (d) => getX(d) + getTime?.(d));
     let yMin = 0;
@@ -63,7 +62,7 @@ export const getStackedXScale = (
   const xScale = scaleLinear();
 
   if (normalize) {
-    xScale.domain(NORMALIZED_X_DOMAIN);
+    xScale.domain(NORMALIZED_VALUE_DOMAIN);
   } else {
     const grouped = group(data, (d) => getY(d) + getTime?.(d));
     let xMin = 0;
diff --git a/app/config-types.ts b/app/config-types.ts
index 7ab27cd18..181876eea 100644
--- a/app/config-types.ts
+++ b/app/config-types.ts
@@ -757,32 +757,6 @@ const ComboLineColumnConfig = t.intersection([
 ]);
 export type ComboLineColumnConfig = t.TypeOf<typeof ComboLineColumnConfig>;
 
-const ComboLineBarFields = t.type({
-  x: GenericField,
-  y: t.type({
-    lineComponentId: t.string,
-    lineAxisOrientation: t.union([t.literal("left"), t.literal("right")]),
-    barComponentId: t.string,
-    palette: t.string,
-    colorMapping: ColorMapping,
-  }),
-});
-
-export type ComboLineBarFields = t.TypeOf<typeof ComboLineBarFields>;
-
-const ComboLineBarConfig = t.intersection([
-  GenericChartConfig,
-  t.type(
-    {
-      chartType: t.literal("comboLineBar"),
-      fields: ComboLineBarFields,
-      interactiveFiltersConfig: InteractiveFiltersConfig,
-    },
-    "ComboLineBarConfig"
-  ),
-]);
-export type ComboLineBarConfig = t.TypeOf<typeof ComboLineBarConfig>;
-
 export type ChartSegmentField =
   | AreaSegmentField
   | ColumnSegmentField
@@ -938,7 +912,12 @@ export const isSegmentInConfig = (
 
 export const isSortingInConfig = (
   chartConfig: ChartConfig
-): chartConfig is AreaConfig | ColumnConfig | LineConfig | PieConfig => {
+): chartConfig is
+  | AreaConfig
+  | ColumnConfig
+  | BarConfig
+  | LineConfig
+  | PieConfig => {
   return ["area", "column", "bar", "line", "pie"].includes(
     chartConfig.chartType
   );
@@ -946,7 +925,12 @@ export const isSortingInConfig = (
 
 export const isAnimationInConfig = (
   chartConfig: ChartConfig
-): chartConfig is ColumnConfig | MapConfig | PieConfig | ScatterPlotConfig => {
+): chartConfig is
+  | ColumnConfig
+  | BarConfig
+  | MapConfig
+  | PieConfig
+  | ScatterPlotConfig => {
   return ["column", "bar", "map", "pie", "scatterplot"].includes(
     chartConfig.chartType
   );
@@ -1029,6 +1013,7 @@ type BarAdjusters = BaseAdjusters<BarConfig> & {
     y: { componentId: FieldAdjuster<BarConfig, string> };
     segment: FieldAdjuster<
       BarConfig,
+      | ColumnSegmentField
       | LineSegmentField
       | AreaSegmentField
       | ScatterPlotSegmentField

From b670ce46ab58d37663b3310cee2feef72c062bc2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Mon, 2 Dec 2024 14:57:32 +0000
Subject: [PATCH 10/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20show=20stack?=
 =?UTF-8?q?ed=20bar=20charts?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/chart-config-ui-options.ts            | 16 ++++++++--------
 .../components/chart-options-selector.tsx        |  6 ++----
 2 files changed, 10 insertions(+), 12 deletions(-)

diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts
index bc96a5cfe..1170c7909 100644
--- a/app/charts/chart-config-ui-options.ts
+++ b/app/charts/chart-config-ui-options.ts
@@ -780,9 +780,9 @@ const chartConfigOptionsUISpec: ChartSpecs = {
         filters: false,
         onChange: (id, { chartConfig, measures }) => {
           if (chartConfig.fields.segment?.type === "stacked") {
-            const yMeasure = measures.find((d) => d.id === id);
+            const xMeasure = measures.find((d) => d.id === id);
 
-            if (disableStacked(yMeasure)) {
+            if (disableStacked(xMeasure)) {
               setWith(chartConfig, "fields.segment.type", "grouped", Object);
 
               if (chartConfig.interactiveFiltersConfig?.calculation) {
@@ -851,15 +851,15 @@ const chartConfigOptionsUISpec: ChartSpecs = {
             chartConfig,
             "fields.segment"
           );
-          const yComponent = components.find(
-            (d) => d.id === chartConfig.fields.y.componentId
+          const xComponent = components.find(
+            (d) => d.id === chartConfig.fields.x.componentId
           );
           setWith(
             chartConfig,
             "fields.segment",
             {
               ...segment,
-              type: disableStacked(yComponent) ? "grouped" : "stacked",
+              type: disableStacked(xComponent) ? "grouped" : "stacked",
             },
             Object
           );
@@ -883,9 +883,9 @@ const chartConfigOptionsUISpec: ChartSpecs = {
           },
           chartSubType: {
             getValues: (chartConfig, dimensions) => {
-              const yId = chartConfig.fields.y.componentId;
-              const yDimension = dimensions.find((d) => d.id === yId);
-              const disabledStacked = disableStacked(yDimension);
+              const xId = chartConfig.fields.x.componentId;
+              const xDimension = dimensions.find((d) => d.id === xId);
+              const disabledStacked = disableStacked(xDimension);
 
               return [
                 {
diff --git a/app/configurator/components/chart-options-selector.tsx b/app/configurator/components/chart-options-selector.tsx
index a0715c6ce..e0f30def9 100644
--- a/app/configurator/components/chart-options-selector.tsx
+++ b/app/configurator/components/chart-options-selector.tsx
@@ -246,7 +246,6 @@ const EncodingOptionsPanel = (props: EncodingOptionsPanelProps) => {
     measures,
     observations,
   } = props;
-  const { chartType } = chartConfig;
   const fieldLabelHint: Record<EncodingFieldType, string> = {
     animation: t({
       id: "controls.select.dimension",
@@ -327,8 +326,7 @@ const EncodingOptionsPanel = (props: EncodingOptionsPanelProps) => {
 
   const hasColorPalette = !!encoding.options?.colorPalette;
 
-  const hasSubOptions =
-    (encoding.options?.chartSubType && chartType === "column") ?? false;
+  const hasSubOptions = encoding.options?.chartSubType ?? false;
 
   return (
     <div
@@ -412,7 +410,7 @@ const EncodingOptionsPanel = (props: EncodingOptionsPanelProps) => {
             chartConfig={chartConfig}
             components={allComponents}
             hasColorPalette={hasColorPalette}
-            hasSubOptions={hasSubOptions}
+            hasSubOptions={!!hasSubOptions}
           />
         )}
       {encoding.options?.imputation?.shouldShow(chartConfig, observations) && (

From 585228155bab2d7dfc6a9a74fd2807329c7efecb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Mon, 2 Dec 2024 15:14:59 +0000
Subject: [PATCH 11/54] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20var=20ad?=
 =?UTF-8?q?justment?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-stacked-state.tsx       | 2 +-
 app/charts/column/columns-stacked-state.tsx | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index d31c50f2f..a869b715d 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -177,7 +177,7 @@ const useBarsStackedState = (
     return Object.fromEntries(
       rollup(
         chartData,
-        (v) => sum(v, (x) => getX(x)),
+        (v) => sum(v, (d) => getX(d)),
         (x) => getY(x)
       )
     );
diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx
index 99cf9e869..bc88957c2 100644
--- a/app/charts/column/columns-stacked-state.tsx
+++ b/app/charts/column/columns-stacked-state.tsx
@@ -178,7 +178,7 @@ const useColumnsStackedState = (
     return Object.fromEntries(
       rollup(
         chartData,
-        (v) => sum(v, (x) => getY(x)),
+        (v) => sum(v, (d) => getY(d)),
         (x) => getX(x)
       )
     );

From 4ba3b89ce7cf9c9439af92d03a3b0e12e67e7855 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Mon, 2 Dec 2024 15:53:43 +0000
Subject: [PATCH 12/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20change=20id?=
 =?UTF-8?q?=20name?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/axis-height-band.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/charts/shared/axis-height-band.tsx b/app/charts/shared/axis-height-band.tsx
index 2d7790dc2..d3ed87de0 100644
--- a/app/charts/shared/axis-height-band.tsx
+++ b/app/charts/shared/axis-height-band.tsx
@@ -38,7 +38,7 @@ export const AxisHeightBand = () => {
       }
 
       const g = renderContainer(ref.current, {
-        id: "axis-width-band-vertical",
+        id: "axis-height-band",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
         render: (g) => g.attr("data-testid", "axis-width-band").call(axis),

From d4209bb63b2bfcba16fe924d397aab8780fc1903 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Mon, 2 Dec 2024 16:07:45 +0000
Subject: [PATCH 13/54] =?UTF-8?q?docs=20=F0=9F=93=91:=20corrected=20var=20?=
 =?UTF-8?q?name?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-stacked-state.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index a869b715d..1fdbc5f40 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -328,7 +328,7 @@ const useBarsStackedState = (
 
   const paddingXScale = useMemo(() => {
     //  When the user can toggle between absolute and relative values, we use the
-    // absolute values to calculate the yScale domain, so that the yScale doesn't
+    // absolute values to calculate the xScale domain, so that the xScale doesn't
     // change when the user toggles between absolute and relative values.
     if (interactiveFiltersConfig?.calculation.active) {
       const scale = getStackedXScale(paddingData, {

From 3757b4084b73e7529ffc06f428e61651e9d2bdad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Mon, 2 Dec 2024 16:08:10 +0000
Subject: [PATCH 14/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20adjusted=20x?=
 =?UTF-8?q?Scale?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-stacked-state.tsx | 2 +-
 app/charts/bar/bars-stacked.tsx       | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index 1fdbc5f40..cf66703eb 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -412,7 +412,7 @@ const useBarsStackedState = (
   yScale.range([0, chartHeight]);
   yScaleInteraction.range([0, chartHeight]);
   yScaleTimeRange.range([0, chartHeight]);
-  xScale.range([chartWidth, 0]);
+  xScale.range([0, chartWidth]);
 
   const isMobile = useIsMobile();
 
diff --git a/app/charts/bar/bars-stacked.tsx b/app/charts/bar/bars-stacked.tsx
index 72a5bb3ae..e7a61b793 100644
--- a/app/charts/bar/bars-stacked.tsx
+++ b/app/charts/bar/bars-stacked.tsx
@@ -25,9 +25,9 @@ export const BarsStacked = () => {
         return {
           key: getRenderingKey(observation, d.key),
           y: yScale(getY(observation)) as number,
-          x: xScale(segment[1]),
+          x: xScale(segment[0]),
           height: bandwidth,
-          width: Math.max(0, xScale(segment[0]) - xScale(segment[1])),
+          width: Math.min(0, xScale(segment[0]) - xScale(segment[1])) * -1,
           color,
         };
       });

From 51f46026e19a21f8a7a3ee66529a73d31cc7d308 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 3 Dec 2024 13:13:40 +0000
Subject: [PATCH 15/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20column,=20ba?=
 =?UTF-8?q?r,=20line,=20area=20and=20scaterplot=20added=20to=20chartConfig?=
 =?UTF-8?q?sPathOverrides?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/index.ts | 32 ++++++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/app/charts/index.ts b/app/charts/index.ts
index ac321e5ec..882890074 100644
--- a/app/charts/index.ts
+++ b/app/charts/index.ts
@@ -1771,6 +1771,10 @@ const chartConfigsPathOverrides: {
   };
 } = {
   column: {
+    bar: {
+      "fields.x.componentId": { path: "fields.y.componentId" },
+      "fields.y.componentId": { path: "fields.x.componentId" },
+    },
     map: {
       "fields.areaLayer.componentId": { path: "fields.x.componentId" },
       "fields.areaLayer.color.componentId": { path: "fields.y.componentId" },
@@ -1799,6 +1803,22 @@ const chartConfigsPathOverrides: {
     },
   },
   bar: {
+    column: {
+      "fields.x.componentId": { path: "fields.y.componentId" },
+      "fields.y.componentId": { path: "fields.x.componentId" },
+    },
+    line: {
+      "fields.x.componentId": { path: "fields.y.componentId" },
+      "fields.y.componentId": { path: "fields.x.componentId" },
+    },
+    area: {
+      "fields.x.componentId": { path: "fields.y.componentId" },
+      "fields.y.componentId": { path: "fields.x.componentId" },
+    },
+    scatterplot: {
+      "fields.x.componentId": { path: "fields.y.componentId" },
+      "fields.y.componentId": { path: "fields.x.componentId" },
+    },
     map: {
       "fields.areaLayer.componentId": { path: "fields.x.componentId" },
       "fields.areaLayer.color.componentId": { path: "fields.y.componentId" },
@@ -1827,6 +1847,10 @@ const chartConfigsPathOverrides: {
     },
   },
   line: {
+    bar: {
+      "fields.x.componentId": { path: "fields.y.componentId" },
+      "fields.y.componentId": { path: "fields.x.componentId" },
+    },
     map: {
       "fields.areaLayer.color.componentId": { path: "fields.y.componentId" },
     },
@@ -1854,6 +1878,10 @@ const chartConfigsPathOverrides: {
     },
   },
   area: {
+    bar: {
+      "fields.x.componentId": { path: "fields.y.componentId" },
+      "fields.y.componentId": { path: "fields.x.componentId" },
+    },
     map: {
       "fields.areaLayer.color.componentId": { path: "fields.y.componentId" },
     },
@@ -1881,6 +1909,10 @@ const chartConfigsPathOverrides: {
     },
   },
   scatterplot: {
+    bar: {
+      "fields.x.componentId": { path: "fields.y.componentId" },
+      "fields.y.componentId": { path: "fields.x.componentId" },
+    },
     map: {
       "fields.areaLayer.color.componentId": { path: "fields.y.componentId" },
     },

From 5300dda43e95c2e21bd032c8e85aca84dd907a99 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 3 Dec 2024 13:50:23 +0000
Subject: [PATCH 16/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20bad=20merge=20confl?=
 =?UTF-8?q?ict=20resolution?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-grouped-state-props.ts |   2 +-
 app/charts/bar/bars-grouped-state.tsx      |  16 +---
 app/charts/bar/bars-grouped.tsx            |  15 ++-
 app/charts/bar/bars-state-props.ts         |   2 +-
 app/charts/bar/bars-state.tsx              |  14 +--
 app/charts/bar/bars.tsx                    |  13 ++-
 app/charts/shared/chart-state.ts           | 106 ++++++++++++++++++---
 7 files changed, 115 insertions(+), 53 deletions(-)

diff --git a/app/charts/bar/bars-grouped-state-props.ts b/app/charts/bar/bars-grouped-state-props.ts
index f81efdae3..7424d1f06 100644
--- a/app/charts/bar/bars-grouped-state-props.ts
+++ b/app/charts/bar/bars-grouped-state-props.ts
@@ -62,7 +62,7 @@ export const useBarsGroupedStateVariables = (
     observations,
   });
   const numericalXErrorVariables = useNumericalXErrorVariables(x, {
-    numericalXVariables,
+    getValue: numericalXVariables.getX,
     dimensions,
     measures,
   });
diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx
index 41b8ea48d..35d1d49e0 100644
--- a/app/charts/bar/bars-grouped-state.tsx
+++ b/app/charts/bar/bars-grouped-state.tsx
@@ -83,10 +83,8 @@ const useBarsGroupedState = (
     xMeasure,
     getY,
     getMinX,
-    showXStandardError,
-    xErrorMeasure,
-    getXError,
     getXErrorRange,
+    getFormattedXUncertainty,
     segmentDimension,
     segmentsByAbbreviationOrLabel,
     getSegment,
@@ -391,14 +389,6 @@ const useBarsGroupedState = (
           topAnchor: !fields.segment,
         });
 
-    const getError = (d: Observation) => {
-      if (!showXStandardError || !getXError || getXError(d) == null) {
-        return;
-      }
-
-      return `${getXError(d)}${xErrorMeasure?.unit ?? ""}`;
-    };
-
     return {
       yAnchor: yAnchorRaw + (placement.y === "bottom" ? 0.5 : -0.5) * bw,
       xAnchor,
@@ -407,7 +397,7 @@ const useBarsGroupedState = (
       datum: {
         label: fields.segment && getSegmentAbbreviationOrLabel(datum),
         value: xValueFormatter(getX(datum)),
-        error: getError(datum),
+        error: getFormattedXUncertainty(datum),
         color: colors(getSegment(datum)) as string,
       },
       values: sortedTooltipValues.map((td) => ({
@@ -415,7 +405,7 @@ const useBarsGroupedState = (
         value: xMeasure.unit
           ? `${formatNumber(getX(td))} ${xMeasure.unit}`
           : formatNumber(getX(td)),
-        error: getError(td),
+        error: getFormattedXUncertainty(td),
         color: colors(getSegment(td)) as string,
       })),
     };
diff --git a/app/charts/bar/bars-grouped.tsx b/app/charts/bar/bars-grouped.tsx
index 57f8cf2dd..8c55d9445 100644
--- a/app/charts/bar/bars-grouped.tsx
+++ b/app/charts/bar/bars-grouped.tsx
@@ -4,9 +4,8 @@ import { GroupedBarsState } from "@/charts/bar/bars-grouped-state";
 import { RenderBarDatum, renderBars } from "@/charts/bar/rendering-utils";
 import { useChartState } from "@/charts/shared/chart-state";
 import {
-  filterWithoutErrors,
-  renderHorizontalWhisker,
   renderContainer,
+  renderHorizontalWhisker,
   RenderHorizontalWhiskerDatum,
 } from "@/charts/shared/rendering-utils";
 import { useTransitionStore } from "@/stores/transition";
@@ -17,24 +16,24 @@ export const ErrorWhiskers = () => {
     xScale,
     yScaleIn,
     getXErrorRange,
-    getXError,
+    getXErrorPresent,
     yScale,
     getSegment,
     grouped,
-    showXStandardError,
+    showXUncertainty,
   } = useChartState() as GroupedBarsState;
   const { margins, width, height } = bounds;
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
   const renderData: RenderHorizontalWhiskerDatum[] = useMemo(() => {
-    if (!getXErrorRange || !showXStandardError) {
+    if (!getXErrorRange || !showXUncertainty) {
       return [];
     }
 
     const bandwidth = yScaleIn.bandwidth();
     return grouped
-      .filter((d) => d[1].some(filterWithoutErrors(getXError)))
+      .filter((d) => d[1].some(getXErrorPresent))
       .flatMap(([segment, observations]) =>
         observations.map((d) => {
           const y0 = yScaleIn(getSegment(d)) as number;
@@ -53,9 +52,9 @@ export const ErrorWhiskers = () => {
   }, [
     getSegment,
     getXErrorRange,
-    getXError,
+    getXErrorPresent,
     grouped,
-    showXStandardError,
+    showXUncertainty,
     xScale,
     yScaleIn,
     yScale,
diff --git a/app/charts/bar/bars-state-props.ts b/app/charts/bar/bars-state-props.ts
index c9fb5344a..e4f7d2490 100644
--- a/app/charts/bar/bars-state-props.ts
+++ b/app/charts/bar/bars-state-props.ts
@@ -57,7 +57,7 @@ export const useBarsStateVariables = (
     observations,
   });
   const numericalXErrorVariables = useNumericalXErrorVariables(x, {
-    numericalXVariables,
+    getValue: numericalXVariables.getX,
     dimensions,
     measures,
   });
diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
index 54c73ef4a..4ce268fa7 100644
--- a/app/charts/bar/bars-state.tsx
+++ b/app/charts/bar/bars-state.tsx
@@ -75,10 +75,8 @@ const useBarsState = (
     xMeasure,
     getY,
     getMinX,
-    showXStandardError,
-    xErrorMeasure,
-    getXError,
     getXErrorRange,
+    getFormattedXUncertainty,
   } = variables;
   const { chartData, scalesData, timeRangeData, paddingData, allData } = data;
   const { fields, interactiveFiltersConfig } = chartConfig;
@@ -244,14 +242,6 @@ const useBarsState = (
         xMeasure.unit
       );
 
-    const getError = (d: Observation) => {
-      if (!showXStandardError || !getXError || getXError(d) === null) {
-        return;
-      }
-
-      return `${getXError(d)}${xErrorMeasure?.unit ?? ""}`;
-    };
-
     const x = getX(d);
 
     return {
@@ -262,7 +252,7 @@ const useBarsState = (
       datum: {
         label: undefined,
         value: x !== null && isNaN(x) ? "-" : `${xValueFormatter(getX(d))}`,
-        error: getError(d),
+        error: getFormattedXUncertainty(d),
         color: "",
       },
       values: undefined,
diff --git a/app/charts/bar/bars.tsx b/app/charts/bar/bars.tsx
index 55e3aa914..39bb341e9 100644
--- a/app/charts/bar/bars.tsx
+++ b/app/charts/bar/bars.tsx
@@ -6,7 +6,6 @@ import { RenderBarDatum, renderBars } from "@/charts/bar/rendering-utils";
 import { useChartState } from "@/charts/shared/chart-state";
 import {
   RenderHorizontalWhiskerDatum,
-  filterWithoutErrors,
   renderContainer,
   renderHorizontalWhisker,
 } from "@/charts/shared/rendering-utils";
@@ -16,12 +15,12 @@ import { useTheme } from "@/themes";
 export const ErrorWhiskers = () => {
   const {
     getY,
-    getXError,
+    getXErrorPresent,
     getXErrorRange,
     chartData,
     yScale,
     xScale,
-    showXStandardError,
+    showXUncertainty,
     bounds,
   } = useChartState() as BarsState;
   const { margins, width, height } = bounds;
@@ -29,12 +28,12 @@ export const ErrorWhiskers = () => {
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
   const renderData: RenderHorizontalWhiskerDatum[] = useMemo(() => {
-    if (!getXErrorRange || !showXStandardError) {
+    if (!getXErrorRange || !showXUncertainty) {
       return [];
     }
 
     const bandwidth = yScale.bandwidth();
-    return chartData.filter(filterWithoutErrors(getXError)).map((d, i) => {
+    return chartData.filter(getXErrorPresent).map((d, i) => {
       const y0 = yScale(getY(d)) as number;
       const barWidth = Math.min(bandwidth, 15);
       const [x1, x2] = getXErrorRange(d);
@@ -50,9 +49,9 @@ export const ErrorWhiskers = () => {
   }, [
     chartData,
     getY,
-    getXError,
+    getXErrorPresent,
     getXErrorRange,
-    showXStandardError,
+    showXUncertainty,
     xScale,
     yScale,
     width,
diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts
index 5c79cd1bb..94757c7ea 100644
--- a/app/charts/shared/chart-state.ts
+++ b/app/charts/shared/chart-state.ts
@@ -410,10 +410,10 @@ export type NumericalYErrorVariables = {
 };
 
 export type NumericalXErrorVariables = {
-  showXStandardError: boolean;
-  xErrorMeasure: Component | undefined;
-  getXError: ((d: Observation) => ObservationValue) | null;
+  showXUncertainty: boolean;
+  getXErrorPresent: (d: Observation) => boolean;
   getXErrorRange: null | ((d: Observation) => [number, number]);
+  getFormattedXUncertainty: (d: Observation) => string | undefined;
 };
 
 export const useNumericalYErrorVariables = (
@@ -531,28 +531,112 @@ export const useNumericalYErrorVariables = (
 export const useNumericalXErrorVariables = (
   x: GenericField,
   {
-    numericalXVariables,
+    getValue,
     dimensions,
     measures,
   }: {
-    numericalXVariables: NumericalXVariables;
+    getValue: NumericalXVariables["getX"];
     dimensions: Dimension[];
     measures: Measure[];
   }
 ): NumericalXErrorVariables => {
   const showXStandardError = get(x, ["showStandardError"], true);
-  const xErrorMeasure = useErrorMeasure(x.componentId, {
+  const xStandardErrorMeasure = useErrorMeasure(x.componentId, {
+    dimensions,
+    measures,
+    type: RelatedDimensionType.StandardError,
+  });
+  const getXStandardError = useErrorVariable(xStandardErrorMeasure);
+
+  const showXConfidenceInterval = get(x, ["showConfidenceInterval"], true);
+  const xConfidenceIntervalUpperMeasure = useErrorMeasure(x.componentId, {
     dimensions,
     measures,
+    type: RelatedDimensionType.ConfidenceUpperBound,
   });
-  const getXErrorRange = useErrorRange(xErrorMeasure, numericalXVariables.getX);
-  const getXError = useErrorVariable(xErrorMeasure);
+  const getXConfidenceIntervalUpper = useErrorVariable(
+    xConfidenceIntervalUpperMeasure
+  );
+  const xConfidenceIntervalLowerMeasure = useErrorMeasure(x.componentId, {
+    dimensions,
+    measures,
+    type: RelatedDimensionType.ConfidenceLowerBound,
+  });
+  const getXConfidenceIntervalLower = useErrorVariable(
+    xConfidenceIntervalLowerMeasure
+  );
+
+  const getXErrorPresent = useCallback(
+    (d: Observation) => {
+      return (
+        (showXStandardError && getXStandardError?.(d) !== null) ||
+        (showXConfidenceInterval &&
+          getXConfidenceIntervalUpper?.(d) !== null &&
+          getXConfidenceIntervalLower?.(d) !== null)
+      );
+    },
+    [
+      showXStandardError,
+      getXStandardError,
+      showXConfidenceInterval,
+      getXConfidenceIntervalUpper,
+      getXConfidenceIntervalLower,
+    ]
+  );
+  const getXErrorRange = useErrorRange(
+    showXStandardError && xStandardErrorMeasure
+      ? xStandardErrorMeasure
+      : xConfidenceIntervalUpperMeasure,
+    showXStandardError && xStandardErrorMeasure
+      ? xStandardErrorMeasure
+      : xConfidenceIntervalLowerMeasure,
+    getValue
+  );
+  const getFormattedXUncertainty = useCallback(
+    (d: Observation) => {
+      if (
+        showXStandardError &&
+        getXStandardError &&
+        getXStandardError(d) !== null
+      ) {
+        const sd = getXStandardError(d);
+        const unit = xStandardErrorMeasure?.unit ?? "";
+        return ` ± ${sd}${unit}`;
+      }
+
+      if (
+        showXConfidenceInterval &&
+        getXConfidenceIntervalUpper &&
+        getXConfidenceIntervalLower &&
+        getXConfidenceIntervalUpper(d) !== null &&
+        getXConfidenceIntervalLower(d) !== null
+      ) {
+        const cil = getXConfidenceIntervalLower(d);
+        const ciu = getXConfidenceIntervalUpper(d);
+        const unit = xConfidenceIntervalUpperMeasure?.unit ?? "";
+        return `, [-${cil}${unit}, +${ciu}${unit}]`;
+      }
+    },
+    [
+      showXStandardError,
+      getXStandardError,
+      showXConfidenceInterval,
+      getXConfidenceIntervalUpper,
+      getXConfidenceIntervalLower,
+      xStandardErrorMeasure?.unit,
+      xConfidenceIntervalUpperMeasure?.unit,
+    ]
+  );
 
   return {
-    showXStandardError,
-    xErrorMeasure,
-    getXError,
+    showXUncertainty:
+      (showXStandardError && !!xStandardErrorMeasure) ||
+      (showXConfidenceInterval &&
+        !!xConfidenceIntervalUpperMeasure &&
+        !!xConfidenceIntervalLowerMeasure),
+    getXErrorPresent,
     getXErrorRange,
+    getFormattedXUncertainty,
   };
 };
 

From 535f4e6b8a28d246edcf85f4d49f4f1441a36b2f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 3 Dec 2024 13:58:51 +0000
Subject: [PATCH 17/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20tests?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/index.spec.ts | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/app/charts/index.spec.ts b/app/charts/index.spec.ts
index cc422c430..289421ce1 100644
--- a/app/charts/index.spec.ts
+++ b/app/charts/index.spec.ts
@@ -182,7 +182,14 @@ describe("initial config", () => {
 
 describe("enabled chart types", () => {
   it("should allow appropriate chart types based on available dimensions", () => {
-    const expectedChartTypes = ["area", "column", "line", "pie", "table"];
+    const expectedChartTypes = [
+      "area",
+      "bar",
+      "column",
+      "line",
+      "pie",
+      "table",
+    ];
     const { enabledChartTypes, possibleChartTypesDict } = getEnabledChartTypes({
       dimensions: bathingWaterData.data.dataCubeByIri
         .dimensions as any as Dimension[],
@@ -212,14 +219,20 @@ describe("enabled chart types", () => {
     ).toBe(true);
   });
 
-  it("should only allow column, map, pie and table if only geo dimensions are available", () => {
+  it("should only allow column, bar, map, pie and table if only geo dimensions are available", () => {
     const { enabledChartTypes, possibleChartTypesDict } = getEnabledChartTypes({
       dimensions: [{ __typename: "GeoShapesDimension" }] as any,
       measures: [{ __typename: "NumericalMeasure" }] as any,
       cubeCount: 1,
     });
 
-    expect(enabledChartTypes.sort()).toEqual(["column", "map", "pie", "table"]);
+    expect(enabledChartTypes.sort()).toEqual([
+      "bar",
+      "column",
+      "map",
+      "pie",
+      "table",
+    ]);
     expect(possibleChartTypesDict["line"].message).toBeDefined();
   });
 

From 51114bd75a2b17051842142e492a4b3cda87caf6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 3 Dec 2024 14:09:33 +0000
Subject: [PATCH 18/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20PR=20reviews?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/chart-state.ts | 3 +--
 app/config-types.ts              | 6 ++++++
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts
index 94757c7ea..74a3949d8 100644
--- a/app/charts/shared/chart-state.ts
+++ b/app/charts/shared/chart-state.ts
@@ -356,7 +356,7 @@ export type NumericalYVariables = {
 
 export const useNumericalYVariables = (
   // Combo charts have their own logic for y scales.
-  chartType: "area" | "column" | "bar" | "line" | "pie" | "scatterplot",
+  chartType: "area" | "column" | "line" | "pie" | "scatterplot",
   y: GenericField,
   { measuresById }: { measuresById: MeasuresById }
 ): NumericalYVariables => {
@@ -378,7 +378,6 @@ export const useNumericalYVariables = (
       switch (chartType) {
         case "area":
         case "column":
-        case "bar":
         case "pie":
           return Math.min(0, min(data, _getY) ?? 0);
         case "line":
diff --git a/app/config-types.ts b/app/config-types.ts
index 204717495..8153cf130 100644
--- a/app/config-types.ts
+++ b/app/config-types.ts
@@ -1008,6 +1008,7 @@ type ColumnAdjusters = BaseAdjusters<ColumnConfig> & {
     y: { componentId: FieldAdjuster<ColumnConfig, string> };
     segment: FieldAdjuster<
       ColumnConfig,
+      | BarSegmentField
       | LineSegmentField
       | AreaSegmentField
       | ScatterPlotSegmentField
@@ -1042,6 +1043,7 @@ type LineAdjusters = BaseAdjusters<LineConfig> & {
     segment: FieldAdjuster<
       LineConfig,
       | ColumnSegmentField
+      | BarSegmentField
       | AreaSegmentField
       | ScatterPlotSegmentField
       | PieSegmentField
@@ -1057,6 +1059,7 @@ type AreaAdjusters = BaseAdjusters<AreaConfig> & {
     segment: FieldAdjuster<
       AreaConfig,
       | ColumnSegmentField
+      | BarSegmentField
       | LineSegmentField
       | ScatterPlotSegmentField
       | PieSegmentField
@@ -1071,6 +1074,7 @@ type ScatterPlotAdjusters = BaseAdjusters<ScatterPlotConfig> & {
     segment: FieldAdjuster<
       ScatterPlotConfig,
       | ColumnSegmentField
+      | BarSegmentField
       | LineSegmentField
       | AreaSegmentField
       | PieSegmentField
@@ -1086,6 +1090,7 @@ type PieAdjusters = BaseAdjusters<PieConfig> & {
     segment: FieldAdjuster<
       PieConfig,
       | ColumnSegmentField
+      | BarSegmentField
       | LineSegmentField
       | AreaSegmentField
       | ScatterPlotSegmentField
@@ -1100,6 +1105,7 @@ type TableAdjusters = {
   fields: FieldAdjuster<
     TableConfig,
     | ColumnSegmentField
+    | BarSegmentField
     | LineSegmentField
     | AreaSegmentField
     | ScatterPlotSegmentField

From ebf6dcfa23839cd494271835dbca9da7311f10e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 3 Dec 2024 14:42:02 +0000
Subject: [PATCH 19/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20add=20map=20?=
 =?UTF-8?q?and=20pie=20configs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/index.ts | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/app/charts/index.ts b/app/charts/index.ts
index 882890074..19c37d9e8 100644
--- a/app/charts/index.ts
+++ b/app/charts/index.ts
@@ -1819,9 +1819,13 @@ const chartConfigsPathOverrides: {
       "fields.x.componentId": { path: "fields.y.componentId" },
       "fields.y.componentId": { path: "fields.x.componentId" },
     },
+    pie: {
+      "fields.segment.componentId": { path: "fields.y.componentId" },
+      "fields.y.componentId": { path: "fields.x.componentId" },
+    },
     map: {
-      "fields.areaLayer.componentId": { path: "fields.x.componentId" },
-      "fields.areaLayer.color.componentId": { path: "fields.y.componentId" },
+      "fields.areaLayer.componentId": { path: "fields.y.componentId" },
+      "fields.areaLayer.color.componentId": { path: "fields.x.componentId" },
     },
     table: {
       fields: { path: "fields.segment" },
@@ -1940,6 +1944,10 @@ const chartConfigsPathOverrides: {
     },
   },
   pie: {
+    bar: {
+      "fields.segment.componentId": { path: "fields.y.componentId" },
+      "fields.y.componentId": { path: "fields.x.componentId" },
+    },
     map: {
       "fields.areaLayer.componentId": { path: "fields.x.componentId" },
       "fields.areaLayer.color.componentId": { path: "fields.y.componentId" },
@@ -1989,6 +1997,10 @@ const chartConfigsPathOverrides: {
       "fields.x.componentId": { path: "fields.areaLayer.componentId" },
       "fields.y.componentId": { path: "fields.areaLayer.color.componentId" },
     },
+    bar: {
+      "fields.x.componentId": { path: "fields.areaLayer.color.componentId" },
+      "fields.y.componentId": { path: "fields.areaLayer.componentId" },
+    },
     line: {
       "fields.y.componentId": { path: "fields.areaLayer.color.componentId" },
     },

From 4e6bf4fcaa0305efa1d527347e91f293abf5621d Mon Sep 17 00:00:00 2001
From: Bartosz Prusinowski <bartosz@interactivethings.com>
Date: Tue, 3 Dec 2024 15:58:32 +0100
Subject: [PATCH 20/54] fix: Bars enter animation

---
 app/charts/bar/rendering-utils.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/charts/bar/rendering-utils.ts b/app/charts/bar/rendering-utils.ts
index 1e7cbb104..ce622bf47 100644
--- a/app/charts/bar/rendering-utils.ts
+++ b/app/charts/bar/rendering-utils.ts
@@ -34,8 +34,8 @@ export const renderBars = (
           .attr("data-index", (_, i) => i)
           .attr("y", (d) => d.y)
           .attr("x", x0)
-          .attr("width", (d) => d.width)
-          .attr("height", 0)
+          .attr("width", 0)
+          .attr("height", (d) => d.height)
           .attr("fill", (d) => d.color)
           .call((enter) =>
             maybeTransition(enter, {

From dda2553341ceee3f896f156b0dfdc879b905d59a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 3 Dec 2024 15:05:53 +0000
Subject: [PATCH 21/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20bar=20<->=20pie=20a?=
 =?UTF-8?q?djuster?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/index.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/charts/index.ts b/app/charts/index.ts
index 19c37d9e8..df806b230 100644
--- a/app/charts/index.ts
+++ b/app/charts/index.ts
@@ -1821,6 +1821,7 @@ const chartConfigsPathOverrides: {
     },
     pie: {
       "fields.segment.componentId": { path: "fields.y.componentId" },
+      "fields.x.componentId": { path: "fields.y.componentId" },
       "fields.y.componentId": { path: "fields.x.componentId" },
     },
     map: {
@@ -1946,6 +1947,7 @@ const chartConfigsPathOverrides: {
   pie: {
     bar: {
       "fields.segment.componentId": { path: "fields.y.componentId" },
+      "fields.x.componentId": { path: "fields.y.componentId" },
       "fields.y.componentId": { path: "fields.x.componentId" },
     },
     map: {

From 65ed293a4314362215f732788e6fa3d6128e2215 Mon Sep 17 00:00:00 2001
From: Bartosz Prusinowski <bartosz@interactivethings.com>
Date: Tue, 3 Dec 2024 16:18:26 +0100
Subject: [PATCH 22/54] fix: Bar -> pie adjuster (pie doesn't have x field,
 segment is carried automatically)

---
 app/charts/index.ts | 20 ++++++++------------
 1 file changed, 8 insertions(+), 12 deletions(-)

diff --git a/app/charts/index.ts b/app/charts/index.ts
index df806b230..c5b98c0ef 100644
--- a/app/charts/index.ts
+++ b/app/charts/index.ts
@@ -1026,8 +1026,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
     },
     fields: {
       x: {
-        componentId: ({ oldValue, newChartConfig, dimensions, measures }) => {
-          measures[0];
+        componentId: ({ oldValue, newChartConfig, dimensions }) => {
           // When switching from a scatterplot, x is a measure.
           if (dimensions.find((d) => d.id === oldValue)) {
             return produce(newChartConfig, (draft) => {
@@ -1039,8 +1038,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
         },
       },
       y: {
-        componentId: ({ oldValue, newChartConfig, dimensions }) => {
-          dimensions[0];
+        componentId: ({ oldValue, newChartConfig }) => {
           return produce(newChartConfig, (draft) => {
             draft.fields.y.componentId = oldValue;
           });
@@ -1054,8 +1052,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
         measures,
       }) => {
         let newSegment: ColumnSegmentField | undefined;
-        const yMeasure = measures.find(
-          (d) => d.id === newChartConfig.fields.y.componentId
+        const xMeasure = measures.find(
+          (d) => d.id === newChartConfig.fields.x.componentId
         );
 
         // When switching from a table chart, a whole fields object is passed as oldValue.
@@ -1070,14 +1068,14 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
             newSegment = {
               ...tableSegment,
               sorting: DEFAULT_SORTING,
-              type: disableStacked(yMeasure) ? "grouped" : "stacked",
+              type: disableStacked(xMeasure) ? "grouped" : "stacked",
             };
           }
           // Otherwise we are dealing with a segment field. We shouldn't take
           // the segment from oldValue if the component has already been used as
-          // x axis.
+          // y axis.
         } else if (
-          newChartConfig.fields.x.componentId !== oldValue.componentId
+          newChartConfig.fields.y.componentId !== oldValue.componentId
         ) {
           const oldSegment = oldValue as Exclude<typeof oldValue, TableFields>;
           newSegment = {
@@ -1089,7 +1087,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
               acceptedValues: COLUMN_SEGMENT_SORTING.map((d) => d.sortingType),
               defaultValue: "byTotalSize",
             }),
-            type: disableStacked(yMeasure) ? "grouped" : "stacked",
+            type: disableStacked(xMeasure) ? "grouped" : "stacked",
           };
         }
 
@@ -1820,8 +1818,6 @@ const chartConfigsPathOverrides: {
       "fields.y.componentId": { path: "fields.x.componentId" },
     },
     pie: {
-      "fields.segment.componentId": { path: "fields.y.componentId" },
-      "fields.x.componentId": { path: "fields.y.componentId" },
       "fields.y.componentId": { path: "fields.x.componentId" },
     },
     map: {

From 0702e198681a5e7b4bef1d8971f3277db0a0deaf Mon Sep 17 00:00:00 2001
From: Bartosz Prusinowski <bartosz@interactivethings.com>
Date: Tue, 3 Dec 2024 16:25:56 +0100
Subject: [PATCH 23/54] fix: Bar -> combo chart adjusters

---
 app/charts/index.ts | 33 +++++++++++++++++++++++----------
 1 file changed, 23 insertions(+), 10 deletions(-)

diff --git a/app/charts/index.ts b/app/charts/index.ts
index c5b98c0ef..2415a5458 100644
--- a/app/charts/index.ts
+++ b/app/charts/index.ts
@@ -1026,9 +1026,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
     },
     fields: {
       x: {
-        componentId: ({ oldValue, newChartConfig, dimensions }) => {
-          // When switching from a scatterplot, x is a measure.
-          if (dimensions.find((d) => d.id === oldValue)) {
+        componentId: ({ oldValue, newChartConfig, measures }) => {
+          if (measures.find((d) => d.id === oldValue)) {
             return produce(newChartConfig, (draft) => {
               draft.fields.x.componentId = oldValue;
             });
@@ -1038,10 +1037,15 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
         },
       },
       y: {
-        componentId: ({ oldValue, newChartConfig }) => {
-          return produce(newChartConfig, (draft) => {
-            draft.fields.y.componentId = oldValue;
-          });
+        componentId: ({ oldValue, newChartConfig, dimensions }) => {
+          // For most charts, y is a measure.
+          if (dimensions.find((d) => d.id === oldValue)) {
+            return produce(newChartConfig, (draft) => {
+              draft.fields.y.componentId = oldValue;
+            });
+          }
+
+          return newChartConfig;
         },
       },
       segment: ({
@@ -1829,16 +1833,16 @@ const chartConfigsPathOverrides: {
     },
     comboLineSingle: {
       "fields.y.componentIds": {
-        path: "fields.y.componentId",
+        path: "fields.x.componentId",
         oldValue: (d: ComboLineSingleFields["y"]["componentIds"]) => d[0],
       },
     },
     comboLineDual: {
-      "fields.y.leftAxisComponentId": { path: "fields.y.componentId" },
+      "fields.y.leftAxisComponentId": { path: "fields.x.componentId" },
     },
     comboLineColumn: {
       "fields.y": {
-        path: "fields.y.componentId",
+        path: "fields.x.componentId",
         oldValue: (d: ComboLineColumnFields["y"]) => {
           return d.lineAxisOrientation === "left"
             ? d.lineComponentId
@@ -2038,6 +2042,9 @@ const chartConfigsPathOverrides: {
     column: {
       "fields.y.componentId": { path: "fields.y.componentIds" },
     },
+    bar: {
+      "fields.x.componentId": { path: "fields.y.componentIds" },
+    },
     line: {
       "fields.y.componentId": { path: "fields.y.componentIds" },
     },
@@ -2068,6 +2075,9 @@ const chartConfigsPathOverrides: {
     column: {
       "fields.y": { path: "fields.y" },
     },
+    bar: {
+      "fields.x": { path: "fields.y" },
+    },
     line: {
       "fields.y": { path: "fields.y" },
     },
@@ -2094,6 +2104,9 @@ const chartConfigsPathOverrides: {
     column: {
       "fields.y": { path: "fields.y" },
     },
+    bar: {
+      "fields.x": { path: "fields.y" },
+    },
     line: {
       "fields.y": { path: "fields.y" },
     },

From 1321900246f596e5f2b0b8c571ea7e9954c1a114 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 3 Dec 2024 16:10:51 +0000
Subject: [PATCH 24/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20y=20label=20cutoff?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/axis-height-band.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/charts/shared/axis-height-band.tsx b/app/charts/shared/axis-height-band.tsx
index d3ed87de0..c079c05eb 100644
--- a/app/charts/shared/axis-height-band.tsx
+++ b/app/charts/shared/axis-height-band.tsx
@@ -55,7 +55,7 @@ export const AxisHeightBand = () => {
         hasNegativeValues ? gridColor : domainColor
       );
       g.selectAll(".tick text")
-        .attr("x", 0)
+        .attr("x", -fontSize)
         .attr("font-size", fontSize)
         .attr("font-family", fontFamily)
         .attr("fill", labelColor)

From eadab83e53554ae07d9acd24dfc75922309f51c6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Thu, 5 Dec 2024 17:46:19 +0000
Subject: [PATCH 25/54] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20ditch=20?=
 =?UTF-8?q?tooltip=20inverted?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/area/areas-state.tsx              |  2 +-
 app/charts/bar/bars-grouped-state.tsx        |  8 +--
 app/charts/bar/bars-stacked-state.tsx        |  8 +--
 app/charts/bar/bars-state.tsx                |  8 +--
 app/charts/column/columns-grouped-state.tsx  |  2 +-
 app/charts/column/columns-stacked-state.tsx  |  2 +-
 app/charts/column/columns-state.tsx          |  2 +-
 app/charts/combo/combo-line-column-state.tsx |  2 +-
 app/charts/combo/combo-line-dual-state.tsx   |  6 +-
 app/charts/combo/combo-line-single-state.tsx |  2 +-
 app/charts/line/lines-state.tsx              |  2 +-
 app/charts/pie/pie-state.tsx                 |  2 +-
 app/charts/scatterplot/scatterplot-state.tsx |  2 +-
 app/charts/shared/interaction/ruler.tsx      |  4 +-
 app/charts/shared/interaction/tooltip.tsx    | 68 ++------------------
 15 files changed, 31 insertions(+), 89 deletions(-)

diff --git a/app/charts/area/areas-state.tsx b/app/charts/area/areas-state.tsx
index 13b7d2154..f4fc35d7a 100644
--- a/app/charts/area/areas-state.tsx
+++ b/app/charts/area/areas-state.tsx
@@ -394,7 +394,7 @@ const useAreasState = (
         xAnchor,
         yAnchor,
         placement,
-        xValue: timeFormatUnit(getX(datum), xDimension.timeUnit),
+        value: timeFormatUnit(getX(datum), xDimension.timeUnit),
         datum: {
           label: fields.segment && getSegmentAbbreviationOrLabel(datum),
           value: yValueFormatter(getY(datum), getIdentityY(datum)),
diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx
index 35d1d49e0..e5dbec0ff 100644
--- a/app/charts/bar/bars-grouped-state.tsx
+++ b/app/charts/bar/bars-grouped-state.tsx
@@ -32,7 +32,7 @@ import {
   CommonChartState,
   InteractiveYTimeRangeState,
 } from "@/charts/shared/chart-state";
-import { TooltipInfoInverted } from "@/charts/shared/interaction/tooltip";
+import { TooltipInfo } from "@/charts/shared/interaction/tooltip";
 import {
   getCenteredTooltipPlacement,
   MOBILE_TOOLTIP_PLACEMENT,
@@ -65,7 +65,7 @@ export type GroupedBarsState = CommonChartState &
     colors: ScaleOrdinal<string, string>;
     getColorLabel: (segment: string) => string;
     grouped: [string, Observation[]][];
-    getAnnotationInfo: (d: Observation) => TooltipInfoInverted;
+    getAnnotationInfo: (d: Observation) => TooltipInfo;
   };
 
 const useBarsGroupedState = (
@@ -357,7 +357,7 @@ const useBarsGroupedState = (
   const isMobile = useIsMobile();
 
   // Tooltip
-  const getAnnotationInfo = (datum: Observation): TooltipInfoInverted => {
+  const getAnnotationInfo = (datum: Observation): TooltipInfo => {
     const bw = yScale.bandwidth();
     const y = getY(datum);
 
@@ -393,7 +393,7 @@ const useBarsGroupedState = (
       yAnchor: yAnchorRaw + (placement.y === "bottom" ? 0.5 : -0.5) * bw,
       xAnchor,
       placement,
-      yValue: getYAbbreviationOrLabel(datum),
+      value: getYAbbreviationOrLabel(datum),
       datum: {
         label: fields.segment && getSegmentAbbreviationOrLabel(datum),
         value: xValueFormatter(getX(datum)),
diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index cf66703eb..5ea229cb9 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -40,7 +40,7 @@ import {
   CommonChartState,
   InteractiveYTimeRangeState,
 } from "@/charts/shared/chart-state";
-import { TooltipInfoInverted } from "@/charts/shared/interaction/tooltip";
+import { TooltipInfo } from "@/charts/shared/interaction/tooltip";
 import {
   getCenteredTooltipPlacement,
   MOBILE_TOOLTIP_PLACEMENT,
@@ -81,7 +81,7 @@ export type StackedBarsState = CommonChartState &
     getAnnotationInfo: (
       d: Observation,
       orderedSegments: string[]
-    ) => TooltipInfoInverted;
+    ) => TooltipInfo;
   };
 
 const useBarsStackedState = (
@@ -418,7 +418,7 @@ const useBarsStackedState = (
 
   // Tooltips
   const getAnnotationInfo = useCallback(
-    (datum: Observation): TooltipInfoInverted => {
+    (datum: Observation): TooltipInfo => {
       const bw = yScale.bandwidth();
       const y = getY(datum);
 
@@ -455,7 +455,7 @@ const useBarsStackedState = (
         yAnchor: yAnchorRaw + (placement.y === "top" ? 0.5 : -0.5) * bw,
         xAnchor,
         placement,
-        yValue: getYAbbreviationOrLabel(datum),
+        value: getYAbbreviationOrLabel(datum),
         datum: {
           label: fields.segment && getSegmentAbbreviationOrLabel(datum),
           value: xValueFormatter(getX(datum), getIdentityX(datum)),
diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
index 4ce268fa7..b158cd749 100644
--- a/app/charts/bar/bars-state.tsx
+++ b/app/charts/bar/bars-state.tsx
@@ -25,7 +25,7 @@ import {
   CommonChartState,
   InteractiveYTimeRangeState,
 } from "@/charts/shared/chart-state";
-import { TooltipInfoInverted } from "@/charts/shared/interaction/tooltip";
+import { TooltipInfo } from "@/charts/shared/interaction/tooltip";
 import {
   getCenteredTooltipPlacement,
   MOBILE_TOOLTIP_PLACEMENT,
@@ -56,7 +56,7 @@ export type BarsState = CommonChartState &
     yScaleInteraction: ScaleBand<string>;
     yScale: ScaleBand<string>;
     minY: string;
-    getAnnotationInfo: (d: Observation) => TooltipInfoInverted;
+    getAnnotationInfo: (d: Observation) => TooltipInfo;
   };
 
 const useBarsState = (
@@ -220,7 +220,7 @@ const useBarsState = (
   const isMobile = useIsMobile();
 
   // Tooltip
-  const getAnnotationInfo = (d: Observation): TooltipInfoInverted => {
+  const getAnnotationInfo = (d: Observation): TooltipInfo => {
     const yAnchor = (yScale(getY(d)) as number) + yScale.bandwidth() * 0.5;
     const xAnchor = isMobile
       ? chartHeight
@@ -248,7 +248,7 @@ const useBarsState = (
       xAnchor,
       yAnchor,
       placement,
-      yValue: yTimeUnit ? timeFormatUnit(yLabel, yTimeUnit) : yLabel,
+      value: yTimeUnit ? timeFormatUnit(yLabel, yTimeUnit) : yLabel,
       datum: {
         label: undefined,
         value: x !== null && isNaN(x) ? "-" : `${xValueFormatter(getX(d))}`,
diff --git a/app/charts/column/columns-grouped-state.tsx b/app/charts/column/columns-grouped-state.tsx
index cab70d69d..736281268 100644
--- a/app/charts/column/columns-grouped-state.tsx
+++ b/app/charts/column/columns-grouped-state.tsx
@@ -400,7 +400,7 @@ const useColumnsGroupedState = (
       xAnchor: xAnchorRaw + (placement.x === "right" ? 0.5 : -0.5) * bw,
       yAnchor,
       placement,
-      xValue: getXAbbreviationOrLabel(datum),
+      value: getXAbbreviationOrLabel(datum),
       datum: {
         label: fields.segment && getSegmentAbbreviationOrLabel(datum),
         value: yValueFormatter(getY(datum)),
diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx
index bc88957c2..16daeb5a1 100644
--- a/app/charts/column/columns-stacked-state.tsx
+++ b/app/charts/column/columns-stacked-state.tsx
@@ -460,7 +460,7 @@ const useColumnsStackedState = (
         xAnchor: xAnchorRaw + (placement.x === "right" ? 0.5 : -0.5) * bw,
         yAnchor,
         placement,
-        xValue: getXAbbreviationOrLabel(datum),
+        value: getXAbbreviationOrLabel(datum),
         datum: {
           label: fields.segment && getSegmentAbbreviationOrLabel(datum),
           value: yValueFormatter(getY(datum), getIdentityY(datum)),
diff --git a/app/charts/column/columns-state.tsx b/app/charts/column/columns-state.tsx
index 97bc98aeb..a6efece3d 100644
--- a/app/charts/column/columns-state.tsx
+++ b/app/charts/column/columns-state.tsx
@@ -249,7 +249,7 @@ const useColumnsState = (
       xAnchor,
       yAnchor,
       placement,
-      xValue: xTimeUnit ? timeFormatUnit(xLabel, xTimeUnit) : xLabel,
+      value: xTimeUnit ? timeFormatUnit(xLabel, xTimeUnit) : xLabel,
       datum: {
         label: undefined,
         value: y !== null && isNaN(y) ? "-" : `${yValueFormatter(getY(d))}`,
diff --git a/app/charts/combo/combo-line-column-state.tsx b/app/charts/combo/combo-line-column-state.tsx
index ed0a6651e..a47804a3d 100644
--- a/app/charts/combo/combo-line-column-state.tsx
+++ b/app/charts/combo/combo-line-column-state.tsx
@@ -204,7 +204,7 @@ const useComboLineColumnState = (
       datum: { label: "", value: "0", color: schemeCategory10[0] },
       xAnchor: xScaled,
       yAnchor,
-      xValue: timeFormatUnit(x, variables.xTimeUnit as TimeUnit),
+      value: timeFormatUnit(x, variables.xTimeUnit as TimeUnit),
       placement,
       values,
     } as TooltipInfo;
diff --git a/app/charts/combo/combo-line-dual-state.tsx b/app/charts/combo/combo-line-dual-state.tsx
index 4a84dd264..15e0ed245 100644
--- a/app/charts/combo/combo-line-dual-state.tsx
+++ b/app/charts/combo/combo-line-dual-state.tsx
@@ -159,12 +159,12 @@ const useComboLineDualState = (
     bottom,
     top: topMarginAxisTitleAdjustment,
   });
-  
+
   const bounds = useChartBounds(width, margins, height, {
     leftLabel: variables.y.left.label,
     rightLabel: variables.y.right.label,
   });
-        
+
   const { chartWidth, chartHeight } = bounds;
   const xScales = [xScale, xScaleTimeRange];
   const yScales = [yScale, yScaleLeft, yScaleRight];
@@ -210,7 +210,7 @@ const useComboLineDualState = (
       datum: { label: "", value: "0", color: schemeCategory10[0] },
       xAnchor: xScaled,
       yAnchor: yAnchor,
-      xValue: timeFormatUnit(x, xDimension.timeUnit),
+      value: timeFormatUnit(x, xDimension.timeUnit),
       placement,
       values,
     } as TooltipInfo;
diff --git a/app/charts/combo/combo-line-single-state.tsx b/app/charts/combo/combo-line-single-state.tsx
index 91837aa7d..11518fa00 100644
--- a/app/charts/combo/combo-line-single-state.tsx
+++ b/app/charts/combo/combo-line-single-state.tsx
@@ -153,7 +153,7 @@ const useComboLineSingleState = (
       datum: { label: "", value: "0", color: schemeCategory10[0] },
       xAnchor: xScaled,
       yAnchor: yAnchor,
-      xValue: timeFormatUnit(x, xDimension.timeUnit),
+      value: timeFormatUnit(x, xDimension.timeUnit),
       placement,
       values,
     } as TooltipInfo;
diff --git a/app/charts/line/lines-state.tsx b/app/charts/line/lines-state.tsx
index 24f219675..312a38563 100644
--- a/app/charts/line/lines-state.tsx
+++ b/app/charts/line/lines-state.tsx
@@ -281,7 +281,7 @@ const useLinesState = (
       xAnchor,
       yAnchor,
       placement,
-      xValue: timeFormatUnit(getX(datum), xDimension.timeUnit),
+      value: timeFormatUnit(getX(datum), xDimension.timeUnit),
       datum: {
         label: fields.segment && getSegmentAbbreviationOrLabel(datum),
         value: yValueFormatter(getY(datum)),
diff --git a/app/charts/pie/pie-state.tsx b/app/charts/pie/pie-state.tsx
index 892ae3dfe..2e6b7b8de 100644
--- a/app/charts/pie/pie-state.tsx
+++ b/app/charts/pie/pie-state.tsx
@@ -240,7 +240,7 @@ const usePieState = (
       xAnchor,
       yAnchor,
       placement: { x: xPlacement, y: yPlacement },
-      xValue: getSegmentAbbreviationOrLabel(datum),
+      value: getSegmentAbbreviationOrLabel(datum),
       datum: {
         value: valueFormatter(getY(datum)),
         color: colors(getSegment(datum)) as string,
diff --git a/app/charts/scatterplot/scatterplot-state.tsx b/app/charts/scatterplot/scatterplot-state.tsx
index 6459b6b74..5eeebb8b2 100644
--- a/app/charts/scatterplot/scatterplot-state.tsx
+++ b/app/charts/scatterplot/scatterplot-state.tsx
@@ -211,7 +211,7 @@ const useScatterplotState = (
       xAnchor,
       yAnchor,
       placement,
-      xValue: formatNumber(getX(datum)),
+      value: formatNumber(getX(datum)),
       tooltipContent: (
         <TooltipScatterplot
           firstLine={fields.segment && getSegmentAbbreviationOrLabel(datum)}
diff --git a/app/charts/shared/interaction/ruler.tsx b/app/charts/shared/interaction/ruler.tsx
index 0d669ecc0..5e440f0f5 100644
--- a/app/charts/shared/interaction/ruler.tsx
+++ b/app/charts/shared/interaction/ruler.tsx
@@ -39,12 +39,12 @@ const RulerInner = (props: RulerInnerProps) => {
     | ComboLineSingleState
     | ComboLineDualState
     | ComboLineColumnState;
-  const { xAnchor, xValue, datum, placement, values } = getAnnotationInfo(d);
+  const { xAnchor, value, datum, placement, values } = getAnnotationInfo(d);
 
   return (
     <RulerContent
       rotate={rotate}
-      xValue={xValue}
+      xValue={value}
       values={values}
       chartHeight={bounds.chartHeight}
       margins={bounds.margins}
diff --git a/app/charts/shared/interaction/tooltip.tsx b/app/charts/shared/interaction/tooltip.tsx
index 07a118879..43ca2c508 100644
--- a/app/charts/shared/interaction/tooltip.tsx
+++ b/app/charts/shared/interaction/tooltip.tsx
@@ -1,6 +1,5 @@
 import { ReactNode } from "react";
 
-import { BarsState } from "@/charts/bar/bars-state";
 import { LinesState } from "@/charts/line/lines-state";
 import { PieState } from "@/charts/pie/pie-state";
 import { useChartState } from "@/charts/shared/chart-state";
@@ -10,9 +9,7 @@ import {
 } from "@/charts/shared/interaction/tooltip-box";
 import {
   TooltipMultiple,
-  TooltipMultipleInverted,
   TooltipSingle,
-  TooltipSingleInverted,
 } from "@/charts/shared/interaction/tooltip-content";
 import { LegendSymbol } from "@/charts/shared/legend-color";
 import { useInteraction } from "@/charts/shared/use-interaction";
@@ -20,7 +17,6 @@ import { Observation } from "@/domain/data";
 
 export const Tooltip = ({
   type = "single",
-  inverted = false,
 }: {
   type: TooltipType;
   inverted?: boolean;
@@ -28,17 +24,7 @@ export const Tooltip = ({
   const [state] = useInteraction();
   const { visible, d } = state.interaction;
 
-  return (
-    <>
-      {visible &&
-        d &&
-        (inverted ? (
-          <TooltipInnerInverted d={d} type={type} />
-        ) : (
-          <TooltipInner d={d} type={type} />
-        ))}
-    </>
-  );
+  return <>{visible && d && <TooltipInner d={d} type={type} />}</>;
 };
 export type { TooltipPlacement };
 
@@ -57,23 +43,13 @@ export interface TooltipInfo {
   xAnchor: number;
   yAnchor: number | undefined;
   placement: TooltipPlacement;
-  xValue: string;
+  value: string;
   tooltipContent?: ReactNode;
   datum: TooltipValue;
   values: TooltipValue[] | undefined;
   withTriangle?: boolean;
 }
 
-export interface TooltipInfoInverted {
-  xAnchor: number;
-  yAnchor: number | undefined;
-  placement: TooltipPlacement;
-  yValue: string;
-  tooltipContent?: ReactNode;
-  datum: TooltipValue;
-  values: TooltipValue[] | undefined;
-}
-
 const TooltipInner = ({ d, type }: { d: Observation; type: TooltipType }) => {
   const { bounds, getAnnotationInfo } = useChartState() as
     | LinesState
@@ -83,7 +59,7 @@ const TooltipInner = ({ d, type }: { d: Observation; type: TooltipType }) => {
     xAnchor,
     yAnchor,
     placement,
-    xValue,
+    value,
     tooltipContent,
     datum,
     values,
@@ -105,10 +81,10 @@ const TooltipInner = ({ d, type }: { d: Observation; type: TooltipType }) => {
       {tooltipContent ? (
         tooltipContent
       ) : type === "multiple" && values ? (
-        <TooltipMultiple xValue={xValue} segmentValues={values} />
+        <TooltipMultiple xValue={value} segmentValues={values} />
       ) : (
         <TooltipSingle
-          xValue={xValue}
+          xValue={value}
           segment={datum.label}
           yValue={datum.value}
           yError={datum.error}
@@ -117,37 +93,3 @@ const TooltipInner = ({ d, type }: { d: Observation; type: TooltipType }) => {
     </TooltipBox>
   );
 };
-
-const TooltipInnerInverted = ({
-  d,
-  type,
-}: {
-  d: Observation;
-  type: TooltipType;
-}) => {
-  const { bounds, getAnnotationInfo } = useChartState() as BarsState;
-  const { margins } = bounds;
-  const { xAnchor, yAnchor, placement, yValue, tooltipContent, datum, values } =
-    getAnnotationInfo(d as any);
-
-  if (Number.isNaN(yAnchor)) {
-    return null;
-  }
-
-  return (
-    <TooltipBox x={xAnchor} y={yAnchor} placement={placement} margins={margins}>
-      {tooltipContent ? (
-        tooltipContent
-      ) : type === "multiple" && values ? (
-        <TooltipMultipleInverted yValue={yValue} segmentValues={values} />
-      ) : (
-        <TooltipSingleInverted
-          yValue={yValue}
-          segment={datum.label}
-          xValue={datum.value}
-          xError={datum.error}
-        />
-      )}
-    </TooltipBox>
-  );
-};

From 893470de23ffac7e6c52841dfeb93c8fd0b7d20e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Thu, 5 Dec 2024 21:05:19 +0000
Subject: [PATCH 26/54] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20got=20ri?=
 =?UTF-8?q?d=20of=20getWideDataInverted?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/area/areas-state.tsx               |   6 +-
 app/charts/bar/bars-stacked-state-props.ts    |   6 +-
 app/charts/bar/bars-stacked-state.tsx         |  10 +-
 .../column/columns-stacked-state-props.ts     |   6 +-
 app/charts/column/columns-stacked-state.tsx   |   6 +-
 app/charts/line/lines-state.tsx               |   6 +-
 app/charts/shared/chart-helpers.tsx           | 200 +++---------------
 app/charts/shared/imputation.tsx              |  53 -----
 8 files changed, 55 insertions(+), 238 deletions(-)

diff --git a/app/charts/area/areas-state.tsx b/app/charts/area/areas-state.tsx
index f4fc35d7a..2816dcc1f 100644
--- a/app/charts/area/areas-state.tsx
+++ b/app/charts/area/areas-state.tsx
@@ -205,9 +205,9 @@ const useAreasState = (
 
   const chartWideData = useMemo(() => {
     return getWideData({
-      dataGroupedByX: chartDataGroupedByX,
-      xKey,
-      getY,
+      dataGrouped: chartDataGroupedByX,
+      key: xKey,
+      getAxisValue: getY,
       getSegment,
       allSegments: segments,
       imputationType: fields.y.imputationType,
diff --git a/app/charts/bar/bars-stacked-state-props.ts b/app/charts/bar/bars-stacked-state-props.ts
index eb0f105ea..2de24ff6b 100644
--- a/app/charts/bar/bars-stacked-state-props.ts
+++ b/app/charts/bar/bars-stacked-state-props.ts
@@ -139,9 +139,9 @@ export const useBarsStackedStateData = (
   const { sortedPlottableData, plottableDataWide } = useMemo(() => {
     const plottableDataByY = group(plottableData, getY);
     const plottableDataWide = getWideData({
-      dataGroupedByX: plottableDataByY,
-      xKey: y.componentId,
-      getY: getX,
+      dataGrouped: plottableDataByY,
+      key: y.componentId,
+      getAxisValue: getX,
       getSegment,
     });
 
diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index 5ea229cb9..1b6dafffb 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -31,7 +31,7 @@ import {
   useChartPadding,
 } from "@/charts/shared/chart-dimensions";
 import {
-  getWideDataInverted,
+  getWideData,
   normalizeDataInverted,
   useGetIdentityX,
 } from "@/charts/shared/chart-helpers";
@@ -200,10 +200,10 @@ const useBarsStackedState = (
   }, [chartData, getX, sumsByY, getY, xMeasure.id, normalize]);
 
   const chartWideData = useMemo(() => {
-    return getWideDataInverted({
-      dataGroupedByY: chartDataGroupedByY,
-      yKey,
-      getX,
+    return getWideData({
+      dataGrouped: chartDataGroupedByY,
+      key: yKey,
+      getAxisValue: getX,
       getSegment,
       allSegments: segments,
       imputationType: "zeros",
diff --git a/app/charts/column/columns-stacked-state-props.ts b/app/charts/column/columns-stacked-state-props.ts
index 008f01587..c99f23d5d 100644
--- a/app/charts/column/columns-stacked-state-props.ts
+++ b/app/charts/column/columns-stacked-state-props.ts
@@ -139,9 +139,9 @@ export const useColumnsStackedStateData = (
   const { sortedPlottableData, plottableDataWide } = useMemo(() => {
     const plottableDataByX = group(plottableData, getX);
     const plottableDataWide = getWideData({
-      dataGroupedByX: plottableDataByX,
-      xKey: x.componentId,
-      getY,
+      dataGrouped: plottableDataByX,
+      key: x.componentId,
+      getAxisValue: getY,
       getSegment,
     });
 
diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx
index 16daeb5a1..be5fc2e14 100644
--- a/app/charts/column/columns-stacked-state.tsx
+++ b/app/charts/column/columns-stacked-state.tsx
@@ -202,9 +202,9 @@ const useColumnsStackedState = (
 
   const chartWideData = useMemo(() => {
     return getWideData({
-      dataGroupedByX: chartDataGroupedByX,
-      xKey,
-      getY,
+      dataGrouped: chartDataGroupedByX,
+      key: xKey,
+      getAxisValue: getY,
       getSegment,
       allSegments: segments,
       imputationType: "zeros",
diff --git a/app/charts/line/lines-state.tsx b/app/charts/line/lines-state.tsx
index 312a38563..db6ba4f5a 100644
--- a/app/charts/line/lines-state.tsx
+++ b/app/charts/line/lines-state.tsx
@@ -122,9 +122,9 @@ const useLinesState = (
   );
 
   const chartWideData = getWideData({
-    dataGroupedByX: preparedDataGroupedByX,
-    xKey,
-    getY,
+    dataGrouped: preparedDataGroupedByX,
+    key: xKey,
+    getAxisValue: getY,
     getSegment,
   });
 
diff --git a/app/charts/shared/chart-helpers.tsx b/app/charts/shared/chart-helpers.tsx
index 08390a218..75a09aecd 100644
--- a/app/charts/shared/chart-helpers.tsx
+++ b/app/charts/shared/chart-helpers.tsx
@@ -6,7 +6,6 @@ import { useCallback, useMemo } from "react";
 import { useMaybeAbbreviations } from "@/charts/shared/abbreviations";
 import {
   imputeTemporalLinearSeries,
-  imputeTemporalLinearSeriesInverted,
   interpolateZerosValue,
 } from "@/charts/shared/imputation";
 import { useObservationLabels } from "@/charts/shared/observation-labels";
@@ -406,16 +405,16 @@ export const stackOffsetDivergingPositiveZeros = (
 // Helper to pivot a dataset to a wider format.
 // Currently, imputation is only applicable to temporal charts (specifically, stacked area charts).
 export const getWideData = ({
-  dataGroupedByX,
-  xKey,
-  getY,
+  dataGrouped,
+  key,
+  getAxisValue,
   allSegments,
   getSegment,
   imputationType = "none",
 }: {
-  dataGroupedByX: InternMap<string, Array<Observation>>;
-  xKey: string;
-  getY: (d: Observation) => number | null;
+  dataGrouped: InternMap<string, Array<Observation>>;
+  key: string;
+  getAxisValue: (d: Observation) => number | null;
   allSegments?: Array<string>;
   getSegment: (d: Observation) => string;
   imputationType?: ImputationType;
@@ -423,38 +422,38 @@ export const getWideData = ({
   switch (imputationType) {
     case "linear":
       if (allSegments) {
-        const dataGroupedByXEntries = [...dataGroupedByX.entries()];
-        const dataGroupedByXWithImputedValues: Array<{
+        const dataGroupedEntries = [...dataGrouped.entries()];
+        const dataGroupedWithImputedValues: Array<{
           [key: string]: number;
-        }> = Array.from({ length: dataGroupedByX.size }, () => ({}));
+        }> = Array.from({ length: dataGrouped.size }, () => ({}));
 
         for (const segment of allSegments) {
           const imputedSeriesValues = imputeTemporalLinearSeries({
-            dataSortedByX: dataGroupedByXEntries.map(([date, values]) => {
+            dataSortedByX: dataGroupedEntries.map(([date, values]) => {
               const observation = values.find((d) => getSegment(d) === segment);
 
               return {
                 date: new Date(date),
-                value: observation ? getY(observation) : null,
+                value: observation ? getAxisValue(observation) : null,
               };
             }),
           });
 
           for (let i = 0; i < imputedSeriesValues.length; i++) {
-            dataGroupedByXWithImputedValues[i][segment] =
+            dataGroupedWithImputedValues[i][segment] =
               imputedSeriesValues[i].value;
           }
         }
 
         return getBaseWideData({
-          dataGroupedByX,
-          xKey,
-          getY,
+          dataGrouped,
+          key,
+          getAxisValue,
           getSegment,
           getOptionalObservationProps: (i) => {
             return allSegments.map((d) => {
               return {
-                [d]: dataGroupedByXWithImputedValues[i][d],
+                [d]: dataGroupedWithImputedValues[i][d],
               };
             });
           },
@@ -463,9 +462,9 @@ export const getWideData = ({
     case "zeros":
       if (allSegments) {
         return getBaseWideData({
-          dataGroupedByX,
-          xKey,
-          getY,
+          dataGrouped,
+          key,
+          getAxisValue,
           getSegment,
           getOptionalObservationProps: () => {
             return allSegments.map((d) => {
@@ -479,180 +478,51 @@ export const getWideData = ({
     case "none":
     default:
       return getBaseWideData({
-        dataGroupedByX,
-        xKey,
-        getY,
-        getSegment,
-      });
-  }
-};
-
-export const getWideDataInverted = ({
-  dataGroupedByY,
-  yKey,
-  getX,
-  allSegments,
-  getSegment,
-  imputationType = "none",
-}: {
-  dataGroupedByY: InternMap<string, Array<Observation>>;
-  yKey: string;
-  getX: (d: Observation) => number | null;
-  allSegments?: Array<string>;
-  getSegment: (d: Observation) => string;
-  imputationType?: ImputationType;
-}) => {
-  switch (imputationType) {
-    case "linear":
-      if (allSegments) {
-        const dataGroupedByYEntries = [...dataGroupedByY.entries()];
-        const dataGroupedByYWithImputedValues: Array<{
-          [key: string]: number;
-        }> = Array.from({ length: dataGroupedByY.size }, () => ({}));
-
-        for (const segment of allSegments) {
-          const imputedSeriesValues = imputeTemporalLinearSeriesInverted({
-            dataSortedByY: dataGroupedByYEntries.map(([date, values]) => {
-              const observation = values.find((d) => getSegment(d) === segment);
-
-              return {
-                date: new Date(date),
-                value: observation ? getX(observation) : null,
-              };
-            }),
-          });
-
-          for (let i = 0; i < imputedSeriesValues.length; i++) {
-            dataGroupedByYWithImputedValues[i][segment] =
-              imputedSeriesValues[i].value;
-          }
-        }
-
-        return getBaseWideDataInverted({
-          dataGroupedByY,
-          yKey,
-          getX,
-          getSegment,
-          getOptionalObservationProps: (i) => {
-            return allSegments.map((d) => {
-              return {
-                [d]: dataGroupedByYWithImputedValues[i][d],
-              };
-            });
-          },
-        });
-      }
-    case "zeros":
-      if (allSegments) {
-        return getBaseWideDataInverted({
-          dataGroupedByY,
-          yKey,
-          getX,
-          getSegment,
-          getOptionalObservationProps: () => {
-            return allSegments.map((d) => {
-              return {
-                [d]: interpolateZerosValue(),
-              };
-            });
-          },
-        });
-      }
-    case "none":
-    default:
-      return getBaseWideDataInverted({
-        dataGroupedByY,
-        yKey,
-        getX,
+        dataGrouped,
+        key,
+        getAxisValue,
         getSegment,
       });
   }
 };
 
 const getBaseWideData = ({
-  dataGroupedByX,
-  xKey,
-  getY,
+  dataGrouped,
+  key,
+  getAxisValue,
   getSegment,
   getOptionalObservationProps = () => [],
 }: {
-  dataGroupedByX: InternMap<string, Array<Observation>>;
-  xKey: string;
-  getY: (d: Observation) => number | null;
+  dataGrouped: InternMap<string, Array<Observation>>;
+  key: string;
+  getAxisValue: (d: Observation) => number | null;
   getSegment: (d: Observation) => string;
   getOptionalObservationProps?: (
     datumIndex: number
   ) => Array<{ [key: string]: number }>;
 }): Array<Observation> => {
   const wideData = [];
-  const dataGroupedByXEntries = [...dataGroupedByX.entries()];
+  const dataGroupedByXEntries = [...dataGrouped.entries()];
 
-  for (let i = 0; i < dataGroupedByX.size; i++) {
+  for (let i = 0; i < dataGrouped.size; i++) {
     const [k, v] = dataGroupedByXEntries[i];
 
     const observation: Observation = Object.assign(
       {
-        [xKey]: k,
-        [`${xKey}/__iri__`]: v[0][`${xKey}/__iri__`],
-        total: sum(v, getY),
-      },
-      ...getOptionalObservationProps(i),
-      ...v
-        // Sorting the values in case of multiple values for the same segment
-        // (desired behavior for getting the domain when time slider is active).
-        .sort((a, b) => {
-          return (getY(a) ?? 0) - (getY(b) ?? 0);
-        })
-        .map((d) => {
-          return {
-            [getSegment(d)]: getY(d),
-          };
-        })
-    );
-
-    wideData.push(observation);
-  }
-
-  return wideData;
-};
-
-const getBaseWideDataInverted = ({
-  dataGroupedByY,
-  yKey,
-  getX,
-  getSegment,
-  getOptionalObservationProps = () => [],
-}: {
-  dataGroupedByY: InternMap<string, Array<Observation>>;
-  yKey: string;
-  getX: (d: Observation) => number | null;
-  getSegment: (d: Observation) => string;
-  getOptionalObservationProps?: (
-    datumIndex: number
-  ) => Array<{ [key: string]: number }>;
-}): Array<Observation> => {
-  const wideData = [];
-  const dataGroupedByYEntries = [...dataGroupedByY.entries()];
-
-  for (let i = 0; i < dataGroupedByY.size; i++) {
-    const [k, v] = dataGroupedByYEntries[i];
-
-    const observation: Observation = Object.assign(
-      {
-        [yKey]: k,
-        [`${yKey}/__iri__`]: v[0][`${yKey}/__iri__`],
-        total: sum(v, getX),
+        [key]: k,
+        [`${key}/__iri__`]: v[0][`${key}/__iri__`],
+        total: sum(v, getAxisValue),
       },
       ...getOptionalObservationProps(i),
       ...v
         // Sorting the values in case of multiple values for the same segment
         // (desired behavior for getting the domain when time slider is active).
         .sort((a, b) => {
-          return (getX(a) ?? 0) - (getX(b) ?? 0);
+          return (getAxisValue(a) ?? 0) - (getAxisValue(b) ?? 0);
         })
         .map((d) => {
           return {
-            [getSegment(d)]: getX(d),
+            [getSegment(d)]: getAxisValue(d),
           };
         })
     );
diff --git a/app/charts/shared/imputation.tsx b/app/charts/shared/imputation.tsx
index d1d69c3cd..25bc1e71e 100644
--- a/app/charts/shared/imputation.tsx
+++ b/app/charts/shared/imputation.tsx
@@ -88,59 +88,6 @@ export const imputeTemporalLinearSeries = ({
   return dataSortedByX as Array<TemporalSeriesAfterImputationEntry>;
 };
 
-export const imputeTemporalLinearSeriesInverted = ({
-  dataSortedByY,
-}: {
-  dataSortedByY: Array<TemporalSeriesBeforeImputationEntry>;
-}): Array<TemporalSeriesAfterImputationEntry> => {
-  const presentDataIndexes = [];
-  const missingDataIndexes = [];
-
-  for (let i = 0; i < dataSortedByY.length; i++) {
-    if (dataSortedByY[i].value !== null) {
-      presentDataIndexes.push(i);
-    } else {
-      missingDataIndexes.push(i);
-    }
-  }
-
-  for (const missingDataIndex of missingDataIndexes) {
-    const nextPresentDataIndex = presentDataIndexes.findIndex(
-      (d) => d > missingDataIndex
-    );
-
-    if (nextPresentDataIndex) {
-      const previousPresentDataIndex = nextPresentDataIndex - 1;
-
-      if (previousPresentDataIndex >= 0) {
-        const previous =
-          dataSortedByY[presentDataIndexes[previousPresentDataIndex]];
-        const next = dataSortedByY[presentDataIndexes[nextPresentDataIndex]];
-
-        dataSortedByY[missingDataIndex] = {
-          date: dataSortedByY[missingDataIndex].date,
-          value: interpolateTemporalLinearValue({
-            previousValue: previous.value!,
-            nextValue: next.value!,
-            previousTime: previous.date.getTime(),
-            nextTime: next.date.getTime(),
-            currentTime: dataSortedByY[missingDataIndex].date.getTime(),
-          }),
-        };
-
-        continue;
-      }
-    }
-
-    dataSortedByY[missingDataIndex] = {
-      date: dataSortedByY[missingDataIndex].date,
-      value: 0,
-    };
-  }
-
-  return dataSortedByY as Array<TemporalSeriesAfterImputationEntry>;
-};
-
 export const isUsingImputation = (chartConfig: ChartConfig): boolean => {
   if (isAreaConfig(chartConfig)) {
     const imputationType = chartConfig.fields.y.imputationType || "";

From 8cf0207e457c22cd2a034025fea04c1a7d6fec7f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Thu, 5 Dec 2024 21:06:15 +0000
Subject: [PATCH 27/54] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20got=20ri?=
 =?UTF-8?q?d=20of=20unused=20code?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../shared/interaction/tooltip-content.tsx    | 68 -------------------
 app/charts/shared/interaction/tooltip.tsx     |  7 +-
 2 files changed, 1 insertion(+), 74 deletions(-)

diff --git a/app/charts/shared/interaction/tooltip-content.tsx b/app/charts/shared/interaction/tooltip-content.tsx
index 9bd4d017d..ac31f967b 100644
--- a/app/charts/shared/interaction/tooltip-content.tsx
+++ b/app/charts/shared/interaction/tooltip-content.tsx
@@ -41,43 +41,6 @@ export const TooltipSingle = ({
   );
 };
 
-export const TooltipSingleInverted = ({
-  yValue,
-  segment,
-  xValue,
-  xError,
-}: {
-  yValue?: string;
-  segment?: string;
-  xValue?: string;
-  xError?: string;
-}) => {
-  return (
-    <Box>
-      {yValue && (
-        <Typography
-          component="div"
-          variant="caption"
-          sx={{ fontWeight: "bold" }}
-        >
-          {yValue}
-        </Typography>
-      )}
-      {segment && (
-        <Typography component="div" variant="caption">
-          {segment}
-        </Typography>
-      )}
-      {xValue && (
-        <Typography component="div" variant="caption">
-          {xValue}
-          {xError ? <> ± {xError}</> : null}
-        </Typography>
-      )}
-    </Box>
-  );
-};
-
 export const TooltipMultiple = ({
   xValue,
   segmentValues,
@@ -109,37 +72,6 @@ export const TooltipMultiple = ({
   );
 };
 
-export const TooltipMultipleInverted = ({
-  yValue,
-  segmentValues,
-}: {
-  yValue?: string;
-  segmentValues: TooltipValue[];
-}) => {
-  return (
-    <Box>
-      {yValue && (
-        <Typography
-          component="div"
-          variant="caption"
-          sx={{ fontWeight: "bold" }}
-        >
-          {yValue}
-        </Typography>
-      )}
-      {segmentValues.map((d, i) => (
-        <LegendItem
-          key={i}
-          item={`${d.label}: ${d.value}${d.error ? ` ± ${d.error}` : ""}`}
-          color={d.color}
-          symbol={d.symbol ?? "square"}
-          usage="tooltip"
-        />
-      ))}
-    </Box>
-  );
-};
-
 // Chart Specific
 export const TooltipScatterplot = ({
   firstLine,
diff --git a/app/charts/shared/interaction/tooltip.tsx b/app/charts/shared/interaction/tooltip.tsx
index 43ca2c508..44d9f336d 100644
--- a/app/charts/shared/interaction/tooltip.tsx
+++ b/app/charts/shared/interaction/tooltip.tsx
@@ -15,12 +15,7 @@ import { LegendSymbol } from "@/charts/shared/legend-color";
 import { useInteraction } from "@/charts/shared/use-interaction";
 import { Observation } from "@/domain/data";
 
-export const Tooltip = ({
-  type = "single",
-}: {
-  type: TooltipType;
-  inverted?: boolean;
-}) => {
+export const Tooltip = ({ type = "single" }: { type: TooltipType }) => {
   const [state] = useInteraction();
   const { visible, d } = state.interaction;
 

From adc84a8190eda66d726d8479ec62c5673cc16e31 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Thu, 5 Dec 2024 21:12:19 +0000
Subject: [PATCH 28/54] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20got=20ri?=
 =?UTF-8?q?d=20of=20normalizeDataInverted?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/area/areas-state.tsx             |  4 +--
 app/charts/bar/bars-stacked-state.tsx       |  8 ++---
 app/charts/column/columns-stacked-state.tsx |  4 +--
 app/charts/shared/chart-helpers.tsx         | 38 ++++-----------------
 4 files changed, 15 insertions(+), 39 deletions(-)

diff --git a/app/charts/area/areas-state.tsx b/app/charts/area/areas-state.tsx
index 2816dcc1f..b87ce2630 100644
--- a/app/charts/area/areas-state.tsx
+++ b/app/charts/area/areas-state.tsx
@@ -190,8 +190,8 @@ const useAreasState = (
     if (normalize) {
       return group(
         normalizeData(chartData, {
-          yKey: yMeasure.id,
-          getY,
+          key: yMeasure.id,
+          getAxisValue: getY,
           getTotalGroupValue: (d) => {
             return sumsByX[getXAsString(d)];
           },
diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index 1b6dafffb..71a7c6612 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -32,7 +32,7 @@ import {
 } from "@/charts/shared/chart-dimensions";
 import {
   getWideData,
-  normalizeDataInverted,
+  normalizeData,
   useGetIdentityX,
 } from "@/charts/shared/chart-helpers";
 import {
@@ -187,9 +187,9 @@ const useBarsStackedState = (
   const chartDataGroupedByY = useMemo(() => {
     if (normalize) {
       return group(
-        normalizeDataInverted(chartData, {
-          xKey: xMeasure.id,
-          getX,
+        normalizeData(chartData, {
+          key: xMeasure.id,
+          getAxisValue: getX,
           getTotalGroupValue: (d) => sumsByY[getY(d)],
         }),
         getY
diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx
index be5fc2e14..d69cfddcc 100644
--- a/app/charts/column/columns-stacked-state.tsx
+++ b/app/charts/column/columns-stacked-state.tsx
@@ -189,8 +189,8 @@ const useColumnsStackedState = (
     if (normalize) {
       return group(
         normalizeData(chartData, {
-          yKey: yMeasure.id,
-          getY,
+          key: yMeasure.id,
+          getAxisValue: getY,
           getTotalGroupValue: (d) => sumsByX[getX(d)],
         }),
         getX
diff --git a/app/charts/shared/chart-helpers.tsx b/app/charts/shared/chart-helpers.tsx
index 75a09aecd..c2005f077 100644
--- a/app/charts/shared/chart-helpers.tsx
+++ b/app/charts/shared/chart-helpers.tsx
@@ -554,47 +554,23 @@ export const useGetIdentityX = (id: string) => {
 export const normalizeData = (
   sortedData: Observation[],
   {
-    yKey,
-    getY,
-    getTotalGroupValue,
-  }: {
-    yKey: string;
-    getY: (d: Observation) => number | null;
-    getTotalGroupValue: (d: Observation) => number;
-  }
-): Observation[] => {
-  return sortedData.map((d) => {
-    const totalGroupValue = getTotalGroupValue(d);
-    const y = getY(d);
-
-    return {
-      ...d,
-      [yKey]: 100 * (y ? y / totalGroupValue : y ?? 0),
-      [getIdentityId(yKey)]: y,
-    };
-  });
-};
-
-export const normalizeDataInverted = (
-  sortedData: Observation[],
-  {
-    xKey,
-    getX,
+    key,
+    getAxisValue,
     getTotalGroupValue,
   }: {
-    xKey: string;
-    getX: (d: Observation) => number | null;
+    key: string;
+    getAxisValue: (d: Observation) => number | null;
     getTotalGroupValue: (d: Observation) => number;
   }
 ): Observation[] => {
   return sortedData.map((d) => {
     const totalGroupValue = getTotalGroupValue(d);
-    const x = getX(d);
+    const axisValue = getAxisValue(d);
 
     return {
       ...d,
-      [xKey]: 100 * (x ? x / totalGroupValue : x ?? 0),
-      [getIdentityId(xKey)]: x,
+      [key]: 100 * (axisValue ? axisValue / totalGroupValue : axisValue ?? 0),
+      [getIdentityId(key)]: axisValue,
     };
   });
 };

From b40eb694fb96419c87a64095e8a22312511bef35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Thu, 5 Dec 2024 21:15:42 +0000
Subject: [PATCH 29/54] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20got=20ri?=
 =?UTF-8?q?d=20of=20getStackedTooltipValueFormatterInverted?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/area/areas-state.tsx             |  4 +-
 app/charts/bar/bars-stacked-state.tsx       |  8 ++--
 app/charts/column/columns-stacked-state.tsx |  4 +-
 app/charts/shared/stacked-helpers.spec.ts   |  4 +-
 app/charts/shared/stacked-helpers.ts        | 45 ++++-----------------
 5 files changed, 17 insertions(+), 48 deletions(-)

diff --git a/app/charts/area/areas-state.tsx b/app/charts/area/areas-state.tsx
index b87ce2630..2246fef23 100644
--- a/app/charts/area/areas-state.tsx
+++ b/app/charts/area/areas-state.tsx
@@ -372,8 +372,8 @@ const useAreasState = (
       });
       const yValueFormatter = getStackedTooltipValueFormatter({
         normalize,
-        yMeasureId: yMeasure.id,
-        yMeasureUnit: yMeasure.unit,
+        measureId: yMeasure.id,
+        measureUnit: yMeasure.unit,
         formatters,
         formatNumber,
       });
diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index 71a7c6612..b9949c460 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -46,7 +46,7 @@ import {
   MOBILE_TOOLTIP_PLACEMENT,
 } from "@/charts/shared/interaction/tooltip-box";
 import {
-  getStackedTooltipValueFormatterInverted,
+  getStackedTooltipValueFormatter,
   getStackedXScale,
 } from "@/charts/shared/stacked-helpers";
 import useChartFormatters from "@/charts/shared/use-chart-formatters";
@@ -430,10 +430,10 @@ const useBarsStackedState = (
         getCategory: getSegment,
         sortingOrder: "asc",
       });
-      const xValueFormatter = getStackedTooltipValueFormatterInverted({
+      const xValueFormatter = getStackedTooltipValueFormatter({
         normalize,
-        xMeasureId: xMeasure.id,
-        xMeasureUnit: xMeasure.unit,
+        measureId: xMeasure.id,
+        measureUnit: xMeasure.unit,
         formatters,
         formatNumber,
       });
diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx
index d69cfddcc..e87ec0887 100644
--- a/app/charts/column/columns-stacked-state.tsx
+++ b/app/charts/column/columns-stacked-state.tsx
@@ -438,8 +438,8 @@ const useColumnsStackedState = (
       });
       const yValueFormatter = getStackedTooltipValueFormatter({
         normalize,
-        yMeasureId: yMeasure.id,
-        yMeasureUnit: yMeasure.unit,
+        measureId: yMeasure.id,
+        measureUnit: yMeasure.unit,
         formatters,
         formatNumber,
       });
diff --git a/app/charts/shared/stacked-helpers.spec.ts b/app/charts/shared/stacked-helpers.spec.ts
index a4f03a28d..b8f453da5 100644
--- a/app/charts/shared/stacked-helpers.spec.ts
+++ b/app/charts/shared/stacked-helpers.spec.ts
@@ -69,8 +69,8 @@ describe("getStackedYScales", () => {
 
 describe("getStackedTooltipValueFormatter", () => {
   const commonStackedTooltipFormatProps = {
-    yMeasureId: "y",
-    yMeasureUnit: "ABC",
+    measureId: "y",
+    measureUnit: "ABC",
     formatters: {},
     formatNumber: (d: NumberValue | null | undefined) => `${d}`,
   };
diff --git a/app/charts/shared/stacked-helpers.ts b/app/charts/shared/stacked-helpers.ts
index 444a8c865..fd55ada76 100644
--- a/app/charts/shared/stacked-helpers.ts
+++ b/app/charts/shared/stacked-helpers.ts
@@ -90,14 +90,14 @@ export const getStackedXScale = (
 
 export const getStackedTooltipValueFormatter = ({
   normalize,
-  yMeasureId,
-  yMeasureUnit,
+  measureId,
+  measureUnit,
   formatters,
   formatNumber,
 }: {
   normalize: boolean;
-  yMeasureId: string;
-  yMeasureUnit: NumericalMeasure["unit"];
+  measureId: string;
+  measureUnit: NumericalMeasure["unit"];
   formatters: { [k: string]: (s: any) => string };
   formatNumber: (d: NumberValue | null | undefined) => string;
 }) => {
@@ -106,46 +106,15 @@ export const getStackedTooltipValueFormatter = ({
       return "-";
     }
 
-    const format = formatters[yMeasureId] ?? formatNumber;
+    const format = formatters[measureId] ?? formatNumber;
 
     if (normalize) {
       const rounded = Math.round(d as number);
-      const fValue = formatNumberWithUnit(dIdentity, format, yMeasureUnit);
+      const fValue = formatNumberWithUnit(dIdentity, format, measureUnit);
 
       return `${rounded}% (${fValue})`;
     }
 
-    return formatNumberWithUnit(d, format, yMeasureUnit);
-  };
-};
-
-export const getStackedTooltipValueFormatterInverted = ({
-  normalize,
-  xMeasureId,
-  xMeasureUnit,
-  formatters,
-  formatNumber,
-}: {
-  normalize: boolean;
-  xMeasureId: string;
-  xMeasureUnit: NumericalMeasure["unit"];
-  formatters: { [k: string]: (s: any) => string };
-  formatNumber: (d: NumberValue | null | undefined) => string;
-}) => {
-  return (d: number | null, dIdentity: number | null) => {
-    if (d === null && dIdentity === null) {
-      return "-";
-    }
-
-    const format = formatters[xMeasureId] ?? formatNumber;
-
-    if (normalize) {
-      const rounded = Math.round(d as number);
-      const fValue = formatNumberWithUnit(dIdentity, format, xMeasureUnit);
-
-      return `${rounded}% (${fValue})`;
-    }
-
-    return formatNumberWithUnit(d, format, xMeasureUnit);
+    return formatNumberWithUnit(d, format, measureUnit);
   };
 };

From 98d0981d95a81334ff10970e924aae23ae750a4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Mon, 9 Dec 2024 09:17:11 +0100
Subject: [PATCH 30/54] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20got=20ri?=
 =?UTF-8?q?d=20of=20useBarChartData?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/area/areas-state-props.ts          |   2 +-
 app/charts/bar/bars-grouped-state-props.ts    |   2 +-
 app/charts/bar/bars-stacked-state-props.ts    |   2 +-
 app/charts/bar/bars-state-props.ts            |   6 +-
 .../column/columns-grouped-state-props.ts     |   2 +-
 .../column/columns-stacked-state-props.ts     |   2 +-
 app/charts/column/columns-state-props.ts      |   2 +-
 .../combo/combo-line-column-state-props.ts    |   2 +-
 .../combo/combo-line-dual-state-props.ts      |   2 +-
 .../combo/combo-line-single-state-props.ts    |   2 +-
 app/charts/line/lines-state-props.ts          |   2 +-
 app/charts/shared/chart-state.ts              | 185 +-----------------
 12 files changed, 18 insertions(+), 193 deletions(-)

diff --git a/app/charts/area/areas-state-props.ts b/app/charts/area/areas-state-props.ts
index 4ae4ae3f8..f8017abad 100644
--- a/app/charts/area/areas-state-props.ts
+++ b/app/charts/area/areas-state-props.ts
@@ -93,7 +93,7 @@ export const useAreasStateData = (
   const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: xDimension.id,
-    getXAsDate: getX,
+    getAxisValueAsDate: getX,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   });
diff --git a/app/charts/bar/bars-grouped-state-props.ts b/app/charts/bar/bars-grouped-state-props.ts
index 7424d1f06..201aecb2a 100644
--- a/app/charts/bar/bars-grouped-state-props.ts
+++ b/app/charts/bar/bars-grouped-state-props.ts
@@ -145,7 +145,7 @@ export const useBarsGroupedStateData = (
   const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: yDimension.id,
-    getXAsDate: getYAsDate,
+    getAxisValueAsDate: getYAsDate,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   });
diff --git a/app/charts/bar/bars-stacked-state-props.ts b/app/charts/bar/bars-stacked-state-props.ts
index 2de24ff6b..dcffa2a00 100644
--- a/app/charts/bar/bars-stacked-state-props.ts
+++ b/app/charts/bar/bars-stacked-state-props.ts
@@ -155,7 +155,7 @@ export const useBarsStackedStateData = (
   const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: yDimension.id,
-    getXAsDate: getYAsDate,
+    getAxisValueAsDate: getYAsDate,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   });
diff --git a/app/charts/bar/bars-state-props.ts b/app/charts/bar/bars-state-props.ts
index e4f7d2490..0a9c40906 100644
--- a/app/charts/bar/bars-state-props.ts
+++ b/app/charts/bar/bars-state-props.ts
@@ -12,8 +12,8 @@ import {
   RenderingVariables,
   SortingVariables,
   useBandYVariables,
-  useBarChartData,
   useBaseVariables,
+  useChartData,
   useInteractiveFiltersVariables,
   useNumericalXErrorVariables,
   useNumericalXVariables,
@@ -122,10 +122,10 @@ export const useBarsStateData = (
   const sortedPlottableData = useMemo(() => {
     return sortData(plottableData);
   }, [sortData, plottableData]);
-  const data = useBarChartData(sortedPlottableData, {
+  const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: yDimension.id,
-    getYAsDate,
+    getAxisValueAsDate: getYAsDate,
     getTimeRangeDate,
   });
 
diff --git a/app/charts/column/columns-grouped-state-props.ts b/app/charts/column/columns-grouped-state-props.ts
index 11642228c..ccf4aa85a 100644
--- a/app/charts/column/columns-grouped-state-props.ts
+++ b/app/charts/column/columns-grouped-state-props.ts
@@ -145,7 +145,7 @@ export const useColumnsGroupedStateData = (
   const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: xDimension.id,
-    getXAsDate,
+    getAxisValueAsDate: getXAsDate,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   });
diff --git a/app/charts/column/columns-stacked-state-props.ts b/app/charts/column/columns-stacked-state-props.ts
index c99f23d5d..678910678 100644
--- a/app/charts/column/columns-stacked-state-props.ts
+++ b/app/charts/column/columns-stacked-state-props.ts
@@ -155,7 +155,7 @@ export const useColumnsStackedStateData = (
   const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: xDimension.id,
-    getXAsDate,
+    getAxisValueAsDate: getXAsDate,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   });
diff --git a/app/charts/column/columns-state-props.ts b/app/charts/column/columns-state-props.ts
index 82e8f7617..428f22f62 100644
--- a/app/charts/column/columns-state-props.ts
+++ b/app/charts/column/columns-state-props.ts
@@ -125,7 +125,7 @@ export const useColumnsStateData = (
   const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: xDimension.id,
-    getXAsDate,
+    getAxisValueAsDate: getXAsDate,
     getTimeRangeDate,
   });
 
diff --git a/app/charts/combo/combo-line-column-state-props.ts b/app/charts/combo/combo-line-column-state-props.ts
index d4e53abc9..355bd5118 100644
--- a/app/charts/combo/combo-line-column-state-props.ts
+++ b/app/charts/combo/combo-line-column-state-props.ts
@@ -172,7 +172,7 @@ export const useComboLineColumnStateData = (
   const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: xDimension.id,
-    getXAsDate,
+    getAxisValueAsDate: getXAsDate,
     getTimeRangeDate,
   });
 
diff --git a/app/charts/combo/combo-line-dual-state-props.ts b/app/charts/combo/combo-line-dual-state-props.ts
index 0394e5a39..337401642 100644
--- a/app/charts/combo/combo-line-dual-state-props.ts
+++ b/app/charts/combo/combo-line-dual-state-props.ts
@@ -136,7 +136,7 @@ export const useComboLineDualStateData = (
   const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: xDimension.id,
-    getXAsDate: getX,
+    getAxisValueAsDate: getX,
     getTimeRangeDate,
   });
 
diff --git a/app/charts/combo/combo-line-single-state-props.ts b/app/charts/combo/combo-line-single-state-props.ts
index 86a3b2e14..135da612f 100644
--- a/app/charts/combo/combo-line-single-state-props.ts
+++ b/app/charts/combo/combo-line-single-state-props.ts
@@ -110,7 +110,7 @@ export const useComboLineSingleStateData = (
   const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: xDimension.id,
-    getXAsDate: getX,
+    getAxisValueAsDate: getX,
     getTimeRangeDate,
   });
 
diff --git a/app/charts/line/lines-state-props.ts b/app/charts/line/lines-state-props.ts
index d16e71da6..bf17083fc 100644
--- a/app/charts/line/lines-state-props.ts
+++ b/app/charts/line/lines-state-props.ts
@@ -111,7 +111,7 @@ export const useLinesStateData = (
   const data = useChartData(sortedPlottableData, {
     chartConfig,
     timeRangeDimensionId: xDimension.id,
-    getXAsDate: getX,
+    getAxisValueAsDate: getX,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   });
diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts
index 74a3949d8..4be5277bc 100644
--- a/app/charts/shared/chart-state.ts
+++ b/app/charts/shared/chart-state.ts
@@ -742,13 +742,13 @@ export const useChartData = (
   {
     chartConfig,
     timeRangeDimensionId,
-    getXAsDate,
+    getAxisValueAsDate,
     getSegmentAbbreviationOrLabel,
     getTimeRangeDate,
   }: {
     chartConfig: ChartConfig;
     timeRangeDimensionId: string | undefined;
-    getXAsDate?: (d: Observation) => Date;
+    getAxisValueAsDate?: (d: Observation) => Date;
     getSegmentAbbreviationOrLabel?: (d: Observation) => string;
     getTimeRangeDate?: (d: Observation) => Date;
   }
@@ -785,7 +785,7 @@ export const useChartData = (
   const { potentialTimeRangeFilterIds } = useDashboardInteractiveFilters();
   const interactiveTimeRangeFilters = useMemo(() => {
     const interactiveTimeRangeFilter: ValuePredicate | null =
-      getXAsDate &&
+      getAxisValueAsDate &&
       interactiveFromTime &&
       interactiveToTime &&
       (interactiveTimeRange?.active ||
@@ -793,189 +793,14 @@ export const useChartData = (
           timeRangeDimensionId &&
           potentialTimeRangeFilterIds.includes(timeRangeDimensionId)))
         ? (d: Observation) => {
-            const time = getXAsDate(d).getTime();
+            const time = getAxisValueAsDate(d).getTime();
             return time >= interactiveFromTime && time <= interactiveToTime;
           }
         : null;
 
     return interactiveTimeRangeFilter ? [interactiveTimeRangeFilter] : [];
   }, [
-    getXAsDate,
-    interactiveFromTime,
-    interactiveToTime,
-    interactiveTimeRange?.active,
-    dashboardFilters?.timeRange.active,
-    timeRangeDimensionId,
-    potentialTimeRangeFilterIds,
-  ]);
-
-  // interactive time slider
-  const animationField = getAnimationField(chartConfig);
-  const dynamicScales = animationField?.dynamicScales ?? true;
-  const animationComponentId = animationField?.componentId ?? "";
-  const getAnimationDate = useTemporalVariable(animationComponentId);
-  const getAnimationOrdinalDate = useStringVariable(animationComponentId);
-  const interactiveTimeSliderFilters = useMemo(() => {
-    const interactiveTimeSliderFilter: ValuePredicate | null =
-      animationField?.componentId && timeSlider.value
-        ? (d: Observation) => {
-            if (timeSlider.type === "interval") {
-              return (
-                getAnimationDate(d).getTime() === timeSlider.value!.getTime()
-              );
-            }
-
-            const ordinalDate = getAnimationOrdinalDate(d);
-            return ordinalDate === timeSlider.value!;
-          }
-        : null;
-
-    return interactiveTimeSliderFilter ? [interactiveTimeSliderFilter] : [];
-  }, [
-    animationField?.componentId,
-    timeSlider.type,
-    timeSlider.value,
-    getAnimationDate,
-    getAnimationOrdinalDate,
-  ]);
-
-  // interactive legend
-  const interactiveLegendFilters = useMemo(() => {
-    const legendItems = Object.keys(categories);
-    const interactiveLegendFilter: ValuePredicate | null =
-      interactiveFiltersConfig?.legend?.active && getSegmentAbbreviationOrLabel
-        ? (d: Observation) => {
-            return !legendItems.includes(getSegmentAbbreviationOrLabel(d));
-          }
-        : null;
-
-    return interactiveLegendFilter ? [interactiveLegendFilter] : [];
-  }, [
-    categories,
-    getSegmentAbbreviationOrLabel,
-    interactiveFiltersConfig?.legend?.active,
-  ]);
-
-  const chartData = useMemo(() => {
-    return observations.filter(
-      overEvery([
-        ...interactiveLegendFilters,
-        ...interactiveTimeRangeFilters,
-        ...interactiveTimeSliderFilters,
-      ])
-    );
-  }, [
-    observations,
-    interactiveLegendFilters,
-    interactiveTimeRangeFilters,
-    interactiveTimeSliderFilters,
-  ]);
-
-  const scalesData = useMemo(() => {
-    if (dynamicScales) {
-      return chartData;
-    } else {
-      return observations.filter(
-        overEvery([...interactiveLegendFilters, ...interactiveTimeRangeFilters])
-      );
-    }
-  }, [
-    dynamicScales,
-    chartData,
-    observations,
-    interactiveLegendFilters,
-    interactiveTimeRangeFilters,
-  ]);
-
-  const segmentData = useMemo(() => {
-    return observations.filter(overEvery(interactiveTimeRangeFilters));
-  }, [observations, interactiveTimeRangeFilters]);
-
-  const timeRangeData = useMemo(() => {
-    return observations.filter(overEvery(timeRangeFilters));
-  }, [observations, timeRangeFilters]);
-
-  const paddingData = useMemo(() => {
-    if (dynamicScales) {
-      return chartData;
-    } else {
-      return observations.filter(overEvery(interactiveLegendFilters));
-    }
-  }, [dynamicScales, chartData, observations, interactiveLegendFilters]);
-
-  return {
-    chartData,
-    scalesData,
-    segmentData,
-    timeRangeData,
-    paddingData,
-  };
-};
-
-export const useBarChartData = (
-  observations: Observation[],
-  {
-    chartConfig,
-    timeRangeDimensionId,
-    getYAsDate,
-    getSegmentAbbreviationOrLabel,
-    getTimeRangeDate,
-  }: {
-    chartConfig: ChartConfig;
-    timeRangeDimensionId: string | undefined;
-    getYAsDate?: (d: Observation) => Date;
-    getSegmentAbbreviationOrLabel?: (d: Observation) => string;
-    getTimeRangeDate?: (d: Observation) => Date;
-  }
-): Omit<ChartStateData, "allData"> => {
-  const { interactiveFiltersConfig } = chartConfig;
-  const categories = useChartInteractiveFilters((d) => d.categories);
-  const timeRange = useChartInteractiveFilters((d) => d.timeRange);
-  const timeSlider = useChartInteractiveFilters((d) => d.timeSlider);
-
-  // time range
-  const interactiveTimeRange = interactiveFiltersConfig?.timeRange;
-  const timeRangeFromTime = interactiveTimeRange?.presets.from
-    ? parseDate(interactiveTimeRange?.presets.from).getTime()
-    : undefined;
-  const timeRangeToTime = interactiveTimeRange?.presets.to
-    ? parseDate(interactiveTimeRange?.presets.to).getTime()
-    : undefined;
-  const timeRangeFilters = useMemo(() => {
-    const timeRangeFilter: ValuePredicate | null =
-      getTimeRangeDate && timeRangeFromTime && timeRangeToTime
-        ? (d: Observation) => {
-            const time = getTimeRangeDate(d).getTime();
-            return time >= timeRangeFromTime && time <= timeRangeToTime;
-          }
-        : null;
-
-    return timeRangeFilter ? [timeRangeFilter] : [];
-  }, [timeRangeFromTime, timeRangeToTime, getTimeRangeDate]);
-
-  // interactive time range
-  const interactiveFromTime = timeRange.from?.getTime();
-  const interactiveToTime = timeRange.to?.getTime();
-  const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs);
-  const { potentialTimeRangeFilterIds } = useDashboardInteractiveFilters();
-  const interactiveTimeRangeFilters = useMemo(() => {
-    const interactiveTimeRangeFilter: ValuePredicate | null =
-      getYAsDate &&
-      interactiveFromTime &&
-      interactiveToTime &&
-      (interactiveTimeRange?.active ||
-        (dashboardFilters?.timeRange.active &&
-          timeRangeDimensionId &&
-          potentialTimeRangeFilterIds.includes(timeRangeDimensionId)))
-        ? (d: Observation) => {
-            const time = getYAsDate(d).getTime();
-            return time >= interactiveFromTime && time <= interactiveToTime;
-          }
-        : null;
-
-    return interactiveTimeRangeFilter ? [interactiveTimeRangeFilter] : [];
-  }, [
-    getYAsDate,
+    getAxisValueAsDate,
     interactiveFromTime,
     interactiveToTime,
     interactiveTimeRange?.active,

From f70052e1043d727ea62f9ea95f48631110c4f4d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Mon, 9 Dec 2024 16:45:48 +0100
Subject: [PATCH 31/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20adjust=20mar?=
 =?UTF-8?q?gins=20and=20scroll=20on=20bar=20charts?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-state.tsx    | 33 +++++++++++++++++++++++---------
 app/charts/shared/containers.tsx | 26 +++++++++++++++++++------
 2 files changed, 44 insertions(+), 15 deletions(-)

diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
index b158cd749..c0c93bac5 100644
--- a/app/charts/bar/bars-state.tsx
+++ b/app/charts/bar/bars-state.tsx
@@ -48,6 +48,8 @@ import { useIsMobile } from "@/utils/use-is-mobile";
 
 import { ChartProps } from "../shared/ChartProps";
 
+export const MIN_BAR_HEIGHT = 16;
+
 export type BarsState = CommonChartState &
   BarsStateVariables &
   InteractiveYTimeRangeState & {
@@ -128,7 +130,6 @@ const useBarsState = (
       .paddingInner(PADDING_INNER)
       .paddingOuter(PADDING_OUTER);
     const yScaleInteraction = scaleBand()
-      //NOTE: not sure if this is the right way to go here
       .domain(bandDomain.reverse())
       .paddingInner(0)
       .paddingOuter(0);
@@ -204,18 +205,29 @@ const useBarsState = (
   const margins = {
     top: 0,
     right,
-    bottom,
-    //NOTE: hardcoded for the moment
-    left: 50 + left,
+    /**
+     * Here we have to switch the left and bottom margins because the margins are calculated
+     * based on the "regular" positioning of the axis
+     * */
+    bottom: left,
+    left: bottom,
   };
 
-  const bounds = useChartBounds(width, margins, height);
+  const barCount = yScale.domain().length;
+
+  // Here we adjust the height to make sure the bars have a minimum height and are legible
+  const adjustedHeight =
+    barCount * MIN_BAR_HEIGHT > height
+      ? barCount * MIN_BAR_HEIGHT
+      : height - margins.bottom;
+
+  const bounds = useChartBounds(width, margins, adjustedHeight);
   const { chartWidth, chartHeight } = bounds;
 
   xScale.range([0, chartWidth]);
-  yScaleInteraction.range([0, chartHeight]);
-  yScaleTimeRange.range([0, chartHeight]);
-  yScale.range([chartHeight, 0]);
+  yScaleInteraction.range([0, adjustedHeight]);
+  yScaleTimeRange.range([0, adjustedHeight]);
+  yScale.range([adjustedHeight, 0]);
 
   const isMobile = useIsMobile();
 
@@ -261,7 +273,10 @@ const useBarsState = (
 
   return {
     chartType: "bar",
-    bounds,
+    bounds: {
+      ...bounds,
+      chartHeight: adjustedHeight,
+    },
     chartData,
     allData,
     xScale,
diff --git a/app/charts/shared/containers.tsx b/app/charts/shared/containers.tsx
index 652cc960c..1f26bdd04 100644
--- a/app/charts/shared/containers.tsx
+++ b/app/charts/shared/containers.tsx
@@ -33,7 +33,10 @@ export const ChartContainer = ({ children }: { children: ReactNode }) => {
       ref={ref}
       aria-hidden="true"
       className={classes.chartContainer}
-      style={{ height: isFreeCanvas ? "initial" : bounds.height }}
+      style={{
+        height: isFreeCanvas ? "initial" : bounds.height,
+        overflow: "scroll",
+      }}
     >
       {children}
     </div>
@@ -44,23 +47,34 @@ export const ChartSvg = ({ children }: { children: ReactNode }) => {
   const ref = useRef<SVGSVGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
-  const { bounds, interactiveFiltersConfig } = useChartState();
-  const { width, height, margins } = bounds;
+  const chartState = useChartState();
+  const { bounds, interactiveFiltersConfig } = chartState;
+
+  const { width, margins, chartHeight } = bounds;
 
   useEffect(() => {
     if (ref.current) {
       // Initialize height on mount.
       if (!ref.current.getAttribute("height")) {
-        ref.current.setAttribute("height", height.toString());
+        ref.current.setAttribute(
+          "height",
+          `${chartHeight + margins.bottom + margins.top}`
+        );
       }
 
       const sel = select(ref.current);
       (enableTransition
         ? sel.transition().duration(transitionDuration)
         : sel
-      ).attr("height", height);
+      ).attr("height", `${chartHeight + margins.bottom + margins.top}`);
     }
-  }, [height, enableTransition, transitionDuration]);
+  }, [
+    chartHeight,
+    margins.bottom,
+    margins.top,
+    enableTransition,
+    transitionDuration,
+  ]);
 
   return (
     <svg

From 987131f0622d3000fed0caf7c9905043776cf140 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Mon, 9 Dec 2024 17:20:39 +0100
Subject: [PATCH 32/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20align=20left?=
 =?UTF-8?q?=20margin=20when=20axes=20are=20flipped?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-state.tsx          |  9 +++------
 app/charts/shared/chart-dimensions.tsx | 12 ++++++++++--
 2 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
index c0c93bac5..c532a17ab 100644
--- a/app/charts/bar/bars-state.tsx
+++ b/app/charts/bar/bars-state.tsx
@@ -200,17 +200,14 @@ const useBarsState = (
     bandDomain: yTimeRangeDomainLabels.every((d) => d === undefined)
       ? yScale.domain()
       : yTimeRangeDomainLabels,
+    isFlipped: true,
   });
   const right = 40;
   const margins = {
     top: 0,
     right,
-    /**
-     * Here we have to switch the left and bottom margins because the margins are calculated
-     * based on the "regular" positioning of the axis
-     * */
-    bottom: left,
-    left: bottom,
+    bottom,
+    left,
   };
 
   const barCount = yScale.domain().length;
diff --git a/app/charts/shared/chart-dimensions.tsx b/app/charts/shared/chart-dimensions.tsx
index b9ee42edc..ea8bc8188 100644
--- a/app/charts/shared/chart-dimensions.tsx
+++ b/app/charts/shared/chart-dimensions.tsx
@@ -27,6 +27,8 @@ type ComputeChartPaddingProps = {
   formatNumber: (n: number) => string;
   bandDomain?: string[];
   normalize?: boolean;
+  //Chart is flipped in the case of bar charts where the position of the axes is inverted
+  isFlipped?: boolean;
 };
 
 const computeChartPadding = (
@@ -43,6 +45,7 @@ const computeChartPadding = (
     bandDomain,
     normalize,
     dashboardFilters,
+    isFlipped,
   } = props;
 
   // Fake ticks to compute maximum tick length as
@@ -67,7 +70,9 @@ const computeChartPadding = (
       !!interactiveFiltersConfig?.timeRange.active) ||
     animationPresent
       ? BRUSH_BOTTOM_SPACE
-      : 48;
+      : isFlipped
+        ? 15 // Eyeballed value
+        : 48;
 
   if (bandDomain?.length) {
     bottom +=
@@ -75,7 +80,7 @@ const computeChartPadding = (
       70;
   }
 
-  return { left, bottom };
+  return isFlipped ? { bottom: left, left: bottom } : { left, bottom };
 };
 
 export const useChartPadding = (props: ComputeChartPaddingProps) => {
@@ -88,6 +93,7 @@ export const useChartPadding = (props: ComputeChartPaddingProps) => {
     formatNumber,
     bandDomain,
     normalize,
+    isFlipped,
   } = props;
   const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs);
   return useMemo(() => {
@@ -101,6 +107,7 @@ export const useChartPadding = (props: ComputeChartPaddingProps) => {
       bandDomain,
       normalize,
       dashboardFilters,
+      isFlipped,
     });
   }, [
     yScale,
@@ -112,6 +119,7 @@ export const useChartPadding = (props: ComputeChartPaddingProps) => {
     bandDomain,
     normalize,
     dashboardFilters,
+    isFlipped,
   ]);
 };
 

From c8995262fc9c6aedd245a2f9c31b1127c2214382 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 09:58:41 +0100
Subject: [PATCH 33/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20add=20missin?=
 =?UTF-8?q?g=20types?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/config-types.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/config-types.ts b/app/config-types.ts
index 8153cf130..8f905c312 100644
--- a/app/config-types.ts
+++ b/app/config-types.ts
@@ -1139,6 +1139,7 @@ type ComboLineDualAdjusters = BaseAdjusters<ComboLineDualConfig> & {
       ComboLineDualConfig,
       | AreaFields
       | ColumnFields
+      | BarFields
       | LineFields
       | MapFields
       | PieFields
@@ -1157,6 +1158,7 @@ type ComboLineColumnAdjusters = BaseAdjusters<ComboLineColumnConfig> & {
       ComboLineColumnConfig,
       | AreaFields
       | ColumnFields
+      | BarFields
       | LineFields
       | MapFields
       | PieFields

From befb5f5b0ec1ae591f68e24f5682afa4fe2f972b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 10:31:14 +0100
Subject: [PATCH 34/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20animate=20the=20cor?=
 =?UTF-8?q?rect=20axis?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/charts/index.ts b/app/charts/index.ts
index 2415a5458..4d68fb484 100644
--- a/app/charts/index.ts
+++ b/app/charts/index.ts
@@ -1105,7 +1105,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
         return produce(newChartConfig, (draft) => {
           // Temporal dimension could be used as X axis, in this case we need to
           // remove the animation.
-          if (newChartConfig.fields.x.componentId !== oldValue?.componentId) {
+          if (newChartConfig.fields.y.componentId !== oldValue?.componentId) {
             draft.fields.animation = oldValue;
           }
         });

From ba2822413153b9a3ee0ac13d88c1fe0e4ec9a08e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 10:46:46 +0100
Subject: [PATCH 35/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20remove=20unwanted?=
 =?UTF-8?q?=20animation?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/axis-height-band.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/app/charts/shared/axis-height-band.tsx b/app/charts/shared/axis-height-band.tsx
index c079c05eb..ec96e8047 100644
--- a/app/charts/shared/axis-height-band.tsx
+++ b/app/charts/shared/axis-height-band.tsx
@@ -55,7 +55,6 @@ export const AxisHeightBand = () => {
         hasNegativeValues ? gridColor : domainColor
       );
       g.selectAll(".tick text")
-        .attr("x", -fontSize)
         .attr("font-size", fontSize)
         .attr("font-family", fontFamily)
         .attr("fill", labelColor)

From 468ff64ae644c1d20779cbf13b40464f4a471297 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 10:46:56 +0100
Subject: [PATCH 36/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20label=20change?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/axis-height-band.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/charts/shared/axis-height-band.tsx b/app/charts/shared/axis-height-band.tsx
index ec96e8047..05abcb064 100644
--- a/app/charts/shared/axis-height-band.tsx
+++ b/app/charts/shared/axis-height-band.tsx
@@ -41,7 +41,7 @@ export const AxisHeightBand = () => {
         id: "axis-height-band",
         transform: `translate(${margins.left} ${margins.top})`,
         transition: { enable: enableTransition, duration: transitionDuration },
-        render: (g) => g.attr("data-testid", "axis-width-band").call(axis),
+        render: (g) => g.attr("data-testid", "axis-height-band").call(axis),
         renderUpdate: (g, opts) =>
           maybeTransition(g, {
             transition: opts.transition,

From 1b90124617a3ed6f82349b34b8cc4fbf5a1e0389 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 14:34:10 +0100
Subject: [PATCH 37/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20add=20i18n?=
 =?UTF-8?q?=20to=20bar=20sorting?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/configurator/components/field-i18n.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/configurator/components/field-i18n.ts b/app/configurator/components/field-i18n.ts
index 0716dbc99..b931f7d8d 100644
--- a/app/configurator/components/field-i18n.ts
+++ b/app/configurator/components/field-i18n.ts
@@ -297,6 +297,7 @@ export function getFieldLabel(field: string): string {
     case "line..byDimensionLabel.asc":
     case "sorting.byDimensionLabel.asc":
       return i18n._(fieldLabels["controls.sorting.byDimensionLabel.ascending"]);
+    case "bar..byAuto.asc":
     case "bar.stacked.byAuto.asc":
     case "bar.grouped.byAuto.asc":
     case "column..byAuto.asc":
@@ -306,6 +307,7 @@ export function getFieldLabel(field: string): string {
     case "line..byAuto.asc":
     case "area..byAuto.asc":
       return i18n._(fieldLabels["controls.sorting.byAuto.ascending"]);
+    case "bar..byAuto.desc":
     case "bar.stacked.byAuto.desc":
     case "bar.grouped.byAuto.desc":
     case "column..byAuto.desc":

From efc90bd9c07a6dde33c7bcf0e19a90a362e06650 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 16:07:17 +0100
Subject: [PATCH 38/54] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20extracte?=
 =?UTF-8?q?d=20MIN=5FBAR=5FHEIGHT=20into=20constants=20file?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-state.tsx | 8 +++++---
 app/charts/bar/constants.ts   | 1 +
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
index c532a17ab..bfba5a1d4 100644
--- a/app/charts/bar/bars-state.tsx
+++ b/app/charts/bar/bars-state.tsx
@@ -14,7 +14,11 @@ import {
   useBarsStateData,
   useBarsStateVariables,
 } from "@/charts/bar/bars-state-props";
-import { PADDING_INNER, PADDING_OUTER } from "@/charts/bar/constants";
+import {
+  MIN_BAR_HEIGHT,
+  PADDING_INNER,
+  PADDING_OUTER,
+} from "@/charts/bar/constants";
 import {
   useChartBounds,
   useChartPadding,
@@ -48,8 +52,6 @@ import { useIsMobile } from "@/utils/use-is-mobile";
 
 import { ChartProps } from "../shared/ChartProps";
 
-export const MIN_BAR_HEIGHT = 16;
-
 export type BarsState = CommonChartState &
   BarsStateVariables &
   InteractiveYTimeRangeState & {
diff --git a/app/charts/bar/constants.ts b/app/charts/bar/constants.ts
index ecad19d48..f55bb0072 100644
--- a/app/charts/bar/constants.ts
+++ b/app/charts/bar/constants.ts
@@ -1,3 +1,4 @@
 export const PADDING_OUTER = 0;
 export const PADDING_INNER = 0.1;
 export const PADDING_WITHIN = 0.1;
+export const MIN_BAR_HEIGHT = 16;

From cb28a3a9efe73ad4a96f80a9b8e0b6800f741822 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 16:08:28 +0100
Subject: [PATCH 39/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20adjust=20sta?=
 =?UTF-8?q?cked=20bars=20height?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-stacked-state.tsx | 33 +++++++++++++++++++--------
 1 file changed, 24 insertions(+), 9 deletions(-)

diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx
index b9949c460..d37f9e6a8 100644
--- a/app/charts/bar/bars-stacked-state.tsx
+++ b/app/charts/bar/bars-stacked-state.tsx
@@ -25,7 +25,11 @@ import {
   useBarsStackedStateData,
   useBarsStackedStateVariables,
 } from "@/charts/bar/bars-stacked-state-props";
-import { PADDING_INNER, PADDING_OUTER } from "@/charts/bar/constants";
+import {
+  MIN_BAR_HEIGHT,
+  PADDING_INNER,
+  PADDING_OUTER,
+} from "@/charts/bar/constants";
 import {
   useChartBounds,
   useChartPadding,
@@ -387,7 +391,6 @@ const useBarsStackedState = (
 
   /** Chart dimensions */
   const { left, bottom } = useChartPadding({
-    //TODO: This is wrong, need to fix
     yScale: paddingXScale,
     width,
     height,
@@ -398,20 +401,29 @@ const useBarsStackedState = (
       ? yScale.domain()
       : yTimeRangeDomainLabels,
     normalize,
+    isFlipped: true,
   });
   const right = 40;
   const margins = {
     top: 0,
     right,
-    bottom,
-    left: 50 + left,
+    bottom: bottom + 30,
+    left,
   };
-  const bounds = useChartBounds(width, margins, height);
+
+  const barCount = yScale.domain().length;
+  // Here we adjust the height to make sure the bars have a minimum height and are legible
+  const adjustedHeight =
+    barCount * MIN_BAR_HEIGHT > height
+      ? barCount * MIN_BAR_HEIGHT
+      : height - margins.bottom;
+
+  const bounds = useChartBounds(width, margins, adjustedHeight);
   const { chartWidth, chartHeight } = bounds;
 
-  yScale.range([0, chartHeight]);
-  yScaleInteraction.range([0, chartHeight]);
-  yScaleTimeRange.range([0, chartHeight]);
+  yScale.range([0, adjustedHeight]);
+  yScaleInteraction.range([0, adjustedHeight]);
+  yScaleTimeRange.range([0, adjustedHeight]);
   xScale.range([0, chartWidth]);
 
   const isMobile = useIsMobile();
@@ -494,7 +506,10 @@ const useBarsStackedState = (
 
   return {
     chartType: "bar",
-    bounds,
+    bounds: {
+      ...bounds,
+      chartHeight: adjustedHeight,
+    },
     chartData,
     allData,
     xScale,

From 90f3132bea46c3f81f697f08a4b8e044e2ad21af Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 16:09:04 +0100
Subject: [PATCH 40/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20adjust=20bot?=
 =?UTF-8?q?tom=20margin?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-state.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
index bfba5a1d4..7cd5e64af 100644
--- a/app/charts/bar/bars-state.tsx
+++ b/app/charts/bar/bars-state.tsx
@@ -208,7 +208,7 @@ const useBarsState = (
   const margins = {
     top: 0,
     right,
-    bottom,
+    bottom: bottom + 30,
     left,
   };
 

From 5d2b5fbfc261ca7f2f2b110a070ea5d9e3d3941b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 16:11:33 +0100
Subject: [PATCH 41/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20adjust=20hor?=
 =?UTF-8?q?izontal=20whiskers?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-grouped.tsx      |  6 +++---
 app/charts/bar/bars.tsx              |  6 +++---
 app/charts/shared/rendering-utils.ts | 28 ++++++++++++++--------------
 3 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/app/charts/bar/bars-grouped.tsx b/app/charts/bar/bars-grouped.tsx
index 8c55d9445..2cf32933f 100644
--- a/app/charts/bar/bars-grouped.tsx
+++ b/app/charts/bar/bars-grouped.tsx
@@ -37,14 +37,14 @@ export const ErrorWhiskers = () => {
       .flatMap(([segment, observations]) =>
         observations.map((d) => {
           const y0 = yScaleIn(getSegment(d)) as number;
-          const barWidth = Math.min(bandwidth, 15);
+          const barHeight = Math.min(bandwidth, 15);
           const [x1, x2] = getXErrorRange(d);
           return {
             key: `${segment}-${getSegment(d)}`,
-            y: (yScale(segment) as number) + y0 + bandwidth / 2 - barWidth / 2,
+            y: (yScale(segment) as number) + y0 + bandwidth / 2 - barHeight / 2,
             x1: xScale(x1),
             x2: xScale(x2),
-            width: barWidth,
+            height: barHeight,
           } as RenderHorizontalWhiskerDatum;
         })
       );
diff --git a/app/charts/bar/bars.tsx b/app/charts/bar/bars.tsx
index 39bb341e9..201197744 100644
--- a/app/charts/bar/bars.tsx
+++ b/app/charts/bar/bars.tsx
@@ -35,14 +35,14 @@ export const ErrorWhiskers = () => {
     const bandwidth = yScale.bandwidth();
     return chartData.filter(getXErrorPresent).map((d, i) => {
       const y0 = yScale(getY(d)) as number;
-      const barWidth = Math.min(bandwidth, 15);
+      const barHeight = Math.min(bandwidth, 15);
       const [x1, x2] = getXErrorRange(d);
       return {
         key: `${i}`,
-        y: y0 + bandwidth / 2 - barWidth / 2,
+        y: y0 + bandwidth / 2 - barHeight / 2,
         x1: xScale(x1),
         x2: xScale(x2),
-        width: barWidth,
+        height: barHeight,
       } as RenderHorizontalWhiskerDatum;
     });
     // eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/app/charts/shared/rendering-utils.ts b/app/charts/shared/rendering-utils.ts
index c3f55f6c5..dd30bf7ff 100644
--- a/app/charts/shared/rendering-utils.ts
+++ b/app/charts/shared/rendering-utils.ts
@@ -168,7 +168,7 @@ export type RenderHorizontalWhiskerDatum = {
   y: number;
   x1: number;
   x2: number;
-  width: number;
+  height: number;
   fill?: string;
   renderMiddleCircle?: boolean;
 };
@@ -305,8 +305,8 @@ export const renderHorizontalWhisker = (
               .attr("class", "top")
               .attr("y", (d) => d.y)
               .attr("x", (d) => d.x2)
-              .attr("width", (d) => d.width)
-              .attr("height", ERROR_WHISKER_SIZE)
+              .attr("width", ERROR_WHISKER_SIZE)
+              .attr("height", (d) => d.height)
               .attr("fill", (d) => d.fill ?? "black")
               .attr("stroke", "none")
           )
@@ -314,10 +314,10 @@ export const renderHorizontalWhisker = (
             g
               .append("rect")
               .attr("class", "middle")
-              .attr("y", (d) => d.y + (d.width - ERROR_WHISKER_SIZE) / 2)
-              .attr("x", (d) => d.x2)
-              .attr("width", ERROR_WHISKER_SIZE)
-              .attr("height", (d) => Math.max(0, d.x1 - d.x2))
+              .attr("y", (d) => d.y + (d.height - ERROR_WHISKER_SIZE) / 2)
+              .attr("x", (d) => d.x1)
+              .attr("width", (d) => Math.abs(Math.min(0, d.x1 - d.x2)))
+              .attr("height", ERROR_WHISKER_SIZE)
               .attr("fill", (d) => d.fill ?? "black")
               .attr("stroke", "none")
           )
@@ -327,8 +327,8 @@ export const renderHorizontalWhisker = (
               .attr("class", "bottom")
               .attr("y", (d) => d.y)
               .attr("x", (d) => d.x1)
-              .attr("width", (d) => d.width)
-              .attr("height", ERROR_WHISKER_SIZE)
+              .attr("width", ERROR_WHISKER_SIZE)
+              .attr("height", (d) => d.height)
               .attr("fill", (d) => d.fill ?? "black")
               .attr("stroke", "none")
           )
@@ -359,15 +359,15 @@ export const renderHorizontalWhisker = (
                   .select(".top")
                   .attr("y", (d) => d.y)
                   .attr("x", (d) => d.x2)
-                  .attr("width", (d) => d.width)
+                  .attr("height", (d) => d.height)
                   .attr("fill", (d) => d.fill ?? "black")
               )
               .call((g) =>
                 g
                   .select(".middle")
-                  .attr("y", (d) => d.y + (d.width - ERROR_WHISKER_SIZE) / 2)
-                  .attr("x", (d) => d.x2)
-                  .attr("height", (d) => Math.max(0, d.x1 - d.x2))
+                  .attr("y", (d) => d.y + (d.height - ERROR_WHISKER_SIZE) / 2)
+                  .attr("x", (d) => d.x1)
+                  .attr("width", (d) => Math.abs(Math.min(0, d.x1 - d.x2)))
                   .attr("fill", (d) => d.fill ?? "black")
               )
               .call((g) =>
@@ -375,7 +375,7 @@ export const renderHorizontalWhisker = (
                   .select(".bottom")
                   .attr("y", (d) => d.y)
                   .attr("x", (d) => d.x1)
-                  .attr("width", (d) => d.width)
+                  .attr("height", (d) => d.height)
                   .attr("fill", (d) => d.fill ?? "black")
               )
               .call((g) =>

From f2919948b0a82b58bc7b96e3a247d7508f17e23f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 16:32:10 +0100
Subject: [PATCH 42/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20adjust=20xSc?=
 =?UTF-8?q?ale=20on=20grouped=20bar=20charts?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-grouped-state.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx
index e5dbec0ff..23ffb9cb1 100644
--- a/app/charts/bar/bars-grouped-state.tsx
+++ b/app/charts/bar/bars-grouped-state.tsx
@@ -250,7 +250,7 @@ const useBarsGroupedState = (
       ) ?? 0,
       0
     );
-    const xScale = scaleLinear().domain([maxValue, minValue]).nice();
+    const xScale = scaleLinear().domain([minValue, maxValue]).nice();
 
     const minPaddingValue = getMinX(paddingData, (d) =>
       getXErrorRange ? getXErrorRange(d)[0] : getX(d)
@@ -352,7 +352,7 @@ const useBarsGroupedState = (
   yScaleInteraction.range([0, chartHeight]);
   yScaleIn.range([0, yScale.bandwidth()]);
   yScaleTimeRange.range([0, chartHeight]);
-  xScale.range([chartWidth, 0]);
+  xScale.range([0, chartWidth]);
 
   const isMobile = useIsMobile();
 

From f3ae19c1bb2257c22df79cfbdcdf9b598de3c760 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 17:35:41 +0100
Subject: [PATCH 43/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20adjust=20mar?=
 =?UTF-8?q?gins=20on=20grouped=20bar=20chart?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-grouped-state.tsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx
index 23ffb9cb1..c4feaaed7 100644
--- a/app/charts/bar/bars-grouped-state.tsx
+++ b/app/charts/bar/bars-grouped-state.tsx
@@ -336,13 +336,14 @@ const useBarsGroupedState = (
     bandDomain: yTimeRangeDomainLabels.every((d) => d === undefined)
       ? yScale.domain()
       : yTimeRangeDomainLabels,
+    isFlipped: true,
   });
   const right = 40;
   const margins = {
     top: 0,
     right,
-    bottom,
-    left: 50 + left,
+    bottom: bottom + 30,
+    left,
   };
   const bounds = useChartBounds(width, margins, height);
   const { chartWidth, chartHeight } = bounds;

From 43ec60bf296f1902409321d44c738855ea1e4b8a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Tue, 10 Dec 2024 17:38:22 +0100
Subject: [PATCH 44/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20adjust=20bot?=
 =?UTF-8?q?tom=20axis=20according=20to=20design?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/axis-width-linear.tsx | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/app/charts/shared/axis-width-linear.tsx b/app/charts/shared/axis-width-linear.tsx
index ee53f38fb..aa8a5626d 100644
--- a/app/charts/shared/axis-width-linear.tsx
+++ b/app/charts/shared/axis-width-linear.tsx
@@ -17,9 +17,8 @@ import { getTextWidth } from "@/utils/get-text-width";
 
 export const AxisWidthLinear = () => {
   const formatNumber = useFormatNumber();
-  const { xScale, bounds, xAxisLabel, xMeasure } = useChartState() as
-    | ScatterplotState
-    | BarsState;
+  const { xScale, bounds, xAxisLabel, xMeasure, chartType } =
+    useChartState() as ScatterplotState | BarsState;
   const { chartWidth, chartHeight, margins } = bounds;
   const {
     labelColor,
@@ -41,7 +40,7 @@ export const AxisWidthLinear = () => {
       const tickValues = xScale.ticks(ticks);
       const axis = axisBottom(xScale)
         .tickValues(tickValues)
-        .tickSizeInner(-chartHeight)
+        .tickSizeInner(-chartHeight - 10)
         .tickSizeOuter(-chartHeight)
         .tickFormat(formatNumber);
       const g = renderContainer(ref.current, {
@@ -57,17 +56,21 @@ export const AxisWidthLinear = () => {
       });
 
       g.selectAll(".tick line")
-        .attr("stroke", gridColor)
+        .attr("y1", 10)
+        .attr("stroke", (_, i) => (i === 0 ? "#000000" : gridColor))
         .attr("stroke-width", 1);
       g.selectAll(".tick text")
         .attr("font-size", labelFontSize)
         .attr("font-family", fontFamily)
         .attr("fill", labelColor)
-        .attr("dy", labelFontSize)
+        .attr("dy", labelFontSize + 10)
         .attr("text-anchor", "middle");
-      g.select("path.domain").attr("stroke", gridColor);
+      g.select("path.domain")
+        .attr("stroke", gridColor)
+        .attr("opacity", chartType === "bar" ? 0 : 1);
     }
   }, [
+    chartType,
     bounds.chartWidth,
     chartHeight,
     enableTransition,
@@ -93,7 +96,7 @@ export const AxisWidthLinear = () => {
     <>
       <foreignObject
         x={margins.left}
-        y={margins.top + chartHeight + 24}
+        y={margins.top + chartHeight + 34}
         width={chartWidth}
         height={height}
         style={{ display: "flex", textAlign: "right" }}

From b479cd17c72e78e8d950aedb0f2661f46c1ffc43 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Wed, 11 Dec 2024 10:45:01 +0100
Subject: [PATCH 45/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20animation=20on=20ba?=
 =?UTF-8?q?r=20chart=20coming=20from=20pie?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/index.ts | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/app/charts/index.ts b/app/charts/index.ts
index 4d68fb484..d6ef4c986 100644
--- a/app/charts/index.ts
+++ b/app/charts/index.ts
@@ -1102,8 +1102,15 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
         });
       },
       animation: ({ oldValue, newChartConfig }) => {
+        if (newChartConfig.chartType !== "bar") {
+          return produce(newChartConfig, (draft) => {
+            if (newChartConfig.fields.x.componentId !== oldValue?.componentId) {
+              draft.fields.animation = oldValue;
+            }
+          });
+        }
         return produce(newChartConfig, (draft) => {
-          // Temporal dimension could be used as X axis, in this case we need to
+          // Temporal dimension could be used as Y axis, in this case we need to
           // remove the animation.
           if (newChartConfig.fields.y.componentId !== oldValue?.componentId) {
             draft.fields.animation = oldValue;

From 49f7756a18615b06bf76f92be9bac768c96ae654 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Wed, 11 Dec 2024 10:48:23 +0100
Subject: [PATCH 46/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20remove=20middle=20c?=
 =?UTF-8?q?ircle=20from=20horizontal=20bar=20whiskers?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/rendering-utils.ts | 20 --------------------
 1 file changed, 20 deletions(-)

diff --git a/app/charts/shared/rendering-utils.ts b/app/charts/shared/rendering-utils.ts
index dd30bf7ff..840c594e3 100644
--- a/app/charts/shared/rendering-utils.ts
+++ b/app/charts/shared/rendering-utils.ts
@@ -332,17 +332,6 @@ export const renderHorizontalWhisker = (
               .attr("fill", (d) => d.fill ?? "black")
               .attr("stroke", "none")
           )
-          .call((g) =>
-            g
-              .filter((d) => d.renderMiddleCircle ?? false)
-              .append("circle")
-              .attr("class", "middle-circle")
-              .attr("cy", (d) => d.y + d.width / 2)
-              .attr("cx", (d) => (d.x1 + d.x2) / 2)
-              .attr("r", ERROR_WHISKER_MIDDLE_CIRCLE_RADIUS)
-              .attr("fill", (d) => d.fill ?? "black")
-              .attr("stroke", "none")
-          )
           .call((enter) =>
             maybeTransition(enter, {
               s: (g) => g.attr("opacity", 1),
@@ -377,15 +366,6 @@ export const renderHorizontalWhisker = (
                   .attr("x", (d) => d.x1)
                   .attr("height", (d) => d.height)
                   .attr("fill", (d) => d.fill ?? "black")
-              )
-              .call((g) =>
-                g
-                  .select(".middle-circle")
-                  .attr("cy", (d) => d.y + d.width / 2)
-                  .attr("cx", (d) => (d.x1 + d.x2) / 2)
-                  .attr("r", ERROR_WHISKER_MIDDLE_CIRCLE_RADIUS)
-                  .attr("fill", (d) => d.fill ?? "black")
-                  .attr("stroke", "none")
               ),
           transition,
         }),

From 52cd631e12b4e6da3894564bfd9c3478c264e4ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Wed, 11 Dec 2024 11:52:03 +0100
Subject: [PATCH 47/54] =?UTF-8?q?refactor=20=E2=99=BB=EF=B8=8F:=20rename?=
 =?UTF-8?q?=20vars?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/rendering-utils.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/app/charts/shared/rendering-utils.ts b/app/charts/shared/rendering-utils.ts
index 840c594e3..62c39fbe8 100644
--- a/app/charts/shared/rendering-utils.ts
+++ b/app/charts/shared/rendering-utils.ts
@@ -302,7 +302,7 @@ export const renderHorizontalWhisker = (
           .call((g) =>
             g
               .append("rect")
-              .attr("class", "top")
+              .attr("class", "right")
               .attr("y", (d) => d.y)
               .attr("x", (d) => d.x2)
               .attr("width", ERROR_WHISKER_SIZE)
@@ -324,7 +324,7 @@ export const renderHorizontalWhisker = (
           .call((g) =>
             g
               .append("rect")
-              .attr("class", "bottom")
+              .attr("class", "left")
               .attr("y", (d) => d.y)
               .attr("x", (d) => d.x1)
               .attr("width", ERROR_WHISKER_SIZE)
@@ -345,7 +345,7 @@ export const renderHorizontalWhisker = (
               .attr("opacity", 1)
               .call((g) =>
                 g
-                  .select(".top")
+                  .select(".right")
                   .attr("y", (d) => d.y)
                   .attr("x", (d) => d.x2)
                   .attr("height", (d) => d.height)
@@ -361,7 +361,7 @@ export const renderHorizontalWhisker = (
               )
               .call((g) =>
                 g
-                  .select(".bottom")
+                  .select(".left")
                   .attr("y", (d) => d.y)
                   .attr("x", (d) => d.x1)
                   .attr("height", (d) => d.height)

From 19f50a02376b044f545582d144689b5e43f0cb63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Wed, 11 Dec 2024 11:53:02 +0100
Subject: [PATCH 48/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20adjust=20error=20wh?=
 =?UTF-8?q?iskers=20position?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/charts/bar/bars.tsx b/app/charts/bar/bars.tsx
index 201197744..ae997fc58 100644
--- a/app/charts/bar/bars.tsx
+++ b/app/charts/bar/bars.tsx
@@ -35,7 +35,7 @@ export const ErrorWhiskers = () => {
     const bandwidth = yScale.bandwidth();
     return chartData.filter(getXErrorPresent).map((d, i) => {
       const y0 = yScale(getY(d)) as number;
-      const barHeight = Math.min(bandwidth, 15);
+      const barHeight = Math.min(bandwidth, 16);
       const [x1, x2] = getXErrorRange(d);
       return {
         key: `${i}`,
@@ -55,7 +55,7 @@ export const ErrorWhiskers = () => {
     xScale,
     yScale,
     width,
-    height,
+    bounds.chartHeight,
   ]);
 
   useEffect(() => {

From f938a52ca58cd5195782b0be8d037f1643242c3f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Wed, 11 Dec 2024 11:57:59 +0100
Subject: [PATCH 49/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20var=20import?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/charts/bar/bars.tsx b/app/charts/bar/bars.tsx
index ae997fc58..18d7e8042 100644
--- a/app/charts/bar/bars.tsx
+++ b/app/charts/bar/bars.tsx
@@ -23,7 +23,7 @@ export const ErrorWhiskers = () => {
     showXUncertainty,
     bounds,
   } = useChartState() as BarsState;
-  const { margins, width, height } = bounds;
+  const { margins, width, chartHeight } = bounds;
   const ref = useRef<SVGGElement>(null);
   const enableTransition = useTransitionStore((state) => state.enable);
   const transitionDuration = useTransitionStore((state) => state.duration);
@@ -55,7 +55,7 @@ export const ErrorWhiskers = () => {
     xScale,
     yScale,
     width,
-    bounds.chartHeight,
+    chartHeight,
   ]);
 
   useEffect(() => {

From 7695a010dc8c9b3e1648f1513f2dbcc4130beb05 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Wed, 11 Dec 2024 16:08:18 +0100
Subject: [PATCH 50/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20min=20x=20value=20c?=
 =?UTF-8?q?ame=20from=20the=20data=20instead=20of=200?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/chart-state.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts
index 4be5277bc..69a8ccd91 100644
--- a/app/charts/shared/chart-state.ts
+++ b/app/charts/shared/chart-state.ts
@@ -324,8 +324,9 @@ export const useNumericalXVariables = (
   const getMinX = useCallback(
     (data: Observation[], _getX: NumericalValueGetter) => {
       switch (chartType) {
-        case "scatterplot":
         case "bar":
+          return Math.min(0, min(data, _getX) ?? 0);
+        case "scatterplot":
           return shouldUseDynamicMinScaleValue(xMeasure.scaleType)
             ? min(data, _getX) ?? 0
             : Math.min(0, min(data, _getX) ?? 0);

From 7cdff4ffb5b017dd91215d29de33189efe2dd75f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Wed, 11 Dec 2024 16:12:29 +0100
Subject: [PATCH 51/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20add=20missin?=
 =?UTF-8?q?g=20bar=20fields=20on=2018n?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/configurator/components/field-i18n.ts | 50 +++++++++++++++--------
 1 file changed, 34 insertions(+), 16 deletions(-)

diff --git a/app/configurator/components/field-i18n.ts b/app/configurator/components/field-i18n.ts
index b931f7d8d..c08e34abf 100644
--- a/app/configurator/components/field-i18n.ts
+++ b/app/configurator/components/field-i18n.ts
@@ -40,6 +40,14 @@ const fieldLabels = {
     id: "controls.column.grouped",
     message: "Grouped",
   }),
+  "controls.bar.stacked": defineMessage({
+    id: "controls.bar.stacked",
+    message: "Stacked",
+  }),
+  "controls.bar.grouped": defineMessage({
+    id: "controls.bar.grouped",
+    message: "Grouped",
+  }),
   "chart.map.layers.base": defineMessage({
     id: "chart.map.layers.base",
     message: "Map Display",
@@ -214,6 +222,7 @@ export function getFieldLabel(field: string): string {
   switch (field) {
     // Visual encodings (left column)
     case "column.x":
+    case "bar.x":
     case "line.x":
     case "area.x":
     case "scatterplot.x":
@@ -228,6 +237,7 @@ export function getFieldLabel(field: string): string {
       return i18n._(fieldLabels["controls.measure"]);
     case "scatterplot.y":
     case "column.y":
+    case "bar.y":
     case "line.y":
     case "area.y":
     case "bar.y":
@@ -236,8 +246,8 @@ export function getFieldLabel(field: string): string {
     case "comboLineColumn.y":
     case "y":
       return i18n._(fieldLabels["controls.axis.vertical"]);
-    case "bar.animation":
     case "column.animation":
+    case "bar.animation":
     case "line.animation":
     case "area.animation":
     case "scatterplot.animation":
@@ -245,8 +255,8 @@ export function getFieldLabel(field: string): string {
     case "map.animation":
     case "animation":
       return i18n._(fieldLabels["controls.animation"]);
-    case "bar.segment":
     case "column.segment":
+    case "bar.segment":
     case "line.segment":
     case "area.segment":
     case "scatterplot.segment":
@@ -285,11 +295,12 @@ export function getFieldLabel(field: string): string {
     case "percent":
       return i18n._(fieldLabels["controls.calculation.percent"]);
 
-    case "bar.stacked.byDimensionLabel.asc":
-    case "bar.grouped.byDimensionLabel.asc":
     case "column..byDimensionLabel.asc":
     case "column.stacked.byDimensionLabel.asc":
     case "column.grouped.byDimensionLabel.asc":
+    case "bar..byDimensionLabel.asc":
+    case "bar.stacked.byDimensionLabel.asc":
+    case "bar.grouped.byDimensionLabel.asc":
     case "area..byDimensionLabel.asc":
     // for existing charts compatibility
     case "area.stacked.byDimensionLabel.asc":
@@ -297,31 +308,32 @@ export function getFieldLabel(field: string): string {
     case "line..byDimensionLabel.asc":
     case "sorting.byDimensionLabel.asc":
       return i18n._(fieldLabels["controls.sorting.byDimensionLabel.ascending"]);
-    case "bar..byAuto.asc":
-    case "bar.stacked.byAuto.asc":
-    case "bar.grouped.byAuto.asc":
     case "column..byAuto.asc":
     case "column.stacked.byAuto.asc":
     case "column.grouped.byAuto.asc":
+    case "bar..byAuto.asc":
+    case "bar.stacked.byAuto.asc":
+    case "bar.grouped.byAuto.asc":
     case "pie..byAuto.asc":
     case "line..byAuto.asc":
     case "area..byAuto.asc":
       return i18n._(fieldLabels["controls.sorting.byAuto.ascending"]);
-    case "bar..byAuto.desc":
-    case "bar.stacked.byAuto.desc":
-    case "bar.grouped.byAuto.desc":
     case "column..byAuto.desc":
     case "column.stacked.byAuto.desc":
     case "column.grouped.byAuto.desc":
+    case "bar..byAuto.desc":
+    case "bar.stacked.byAuto.desc":
+    case "bar.grouped.byAuto.desc":
     case "pie..byAuto.desc":
     case "line..byAuto.desc":
     case "area..byAuto.desc":
       return i18n._(fieldLabels["controls.sorting.byAuto.descending"]);
-    case "bar.stacked.byDimensionLabel.desc":
-    case "bar.grouped.byDimensionLabel.desc":
     case "column..byDimensionLabel.desc":
     case "column.stacked.byDimensionLabel.desc":
     case "column.grouped.byDimensionLabel.desc":
+    case "bar..byDimensionLabel.desc":
+    case "bar.stacked.byDimensionLabel.desc":
+    case "bar.grouped.byDimensionLabel.desc":
     case "area..byDimensionLabel.desc":
     // for existing charts compatibility
     case "area.stacked.byDimensionLabel.desc":
@@ -331,33 +343,39 @@ export function getFieldLabel(field: string): string {
       return i18n._(
         fieldLabels["controls.sorting.byDimensionLabel.descending"]
       );
-    case "bar.stacked.byTotalSize.desc":
-    case "bar.grouped.byTotalSize.desc":
     case "column.grouped.byTotalSize.asc":
+    case "bar.grouped.byTotalSize.asc":
       return i18n._(fieldLabels["controls.sorting.byTotalSize.ascending"]);
     case "column.grouped.byTotalSize.desc":
-    case "bar.stacked.byTotalSize.asc":
-    case "bar.grouped.byTotalSize.asc":
+    case "bar.grouped.byTotalSize.desc":
       return i18n._(fieldLabels["controls.sorting.byTotalSize.largestFirst"]);
     case "area..byTotalSize.asc":
     // for existing charts compatibility
     case "area.stacked.byTotalSize.asc":
     case "column.stacked.byTotalSize.asc":
+    case "bar.stacked.byTotalSize.asc":
       return i18n._(fieldLabels["controls.sorting.byTotalSize.largestTop"]);
     case "area..byTotalSize.desc":
     // for existing charts compatibility
     case "area.stacked.byTotalSize.desc":
     case "column.stacked.byTotalSize.desc":
+    case "bar.stacked.byTotalSize.desc":
       return i18n._(fieldLabels["controls.sorting.byTotalSize.largestBottom"]);
     case "column..byMeasure.asc":
     case "column.stacked.byMeasure.asc":
     case "column.grouped.byMeasure.asc":
+    case "bar..byMeasure.asc":
+    case "bar.stacked.byMeasure.asc":
+    case "bar.grouped.byMeasure.asc":
     case "pie..byMeasure.asc":
     case "sorting.byMeasure.asc":
       return i18n._(fieldLabels["controls.sorting.byMeasure.ascending"]);
     case "column..byMeasure.desc":
     case "column.stacked.byMeasure.desc":
     case "column.grouped.byMeasure.desc":
+    case "bar..byMeasure.desc":
+    case "bar.stacked.byMeasure.desc":
+    case "bar.grouped.byMeasure.desc":
     case "pie..byMeasure.desc":
     case "sorting.byMeasure.desc":
       return i18n._(fieldLabels["controls.sorting.byMeasure.descending"]);

From 9c902df94976ffd8419c861e93c6fbe1346b7459 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Wed, 11 Dec 2024 16:38:43 +0100
Subject: [PATCH 52/54] =?UTF-8?q?feat=20=E2=9A=A1=EF=B8=8F:=20remove=20bar?=
 =?UTF-8?q?=20instead=20of=20hiding=20it?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/shared/axis-width-linear.tsx | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/app/charts/shared/axis-width-linear.tsx b/app/charts/shared/axis-width-linear.tsx
index aa8a5626d..5a22fe3bf 100644
--- a/app/charts/shared/axis-width-linear.tsx
+++ b/app/charts/shared/axis-width-linear.tsx
@@ -65,9 +65,10 @@ export const AxisWidthLinear = () => {
         .attr("fill", labelColor)
         .attr("dy", labelFontSize + 10)
         .attr("text-anchor", "middle");
-      g.select("path.domain")
-        .attr("stroke", gridColor)
-        .attr("opacity", chartType === "bar" ? 0 : 1);
+      g.select("path.domain").attr("stroke", gridColor);
+      if (chartType === "bar") {
+        g.select("path.domain").remove();
+      }
     }
   }, [
     chartType,

From a3ef3e30047edc740ca7a723ae1a5243316c3b92 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Wed, 11 Dec 2024 17:01:39 +0100
Subject: [PATCH 53/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20bars=20not=20adjust?=
 =?UTF-8?q?ing=20when=20width=20changed?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars.tsx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/charts/bar/bars.tsx b/app/charts/bar/bars.tsx
index 18d7e8042..5522a0134 100644
--- a/app/charts/bar/bars.tsx
+++ b/app/charts/bar/bars.tsx
@@ -112,7 +112,9 @@ export const Bars = () => {
         color,
       };
     });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [
+    bounds.width,
     chartData,
     bandwidth,
     getX,

From e7e620a39ea3292be8e83be6725b14af235b111b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joa=CC=83o=20Sobral?= <joao.tiago.sobral@gmail.com>
Date: Wed, 11 Dec 2024 17:15:37 +0100
Subject: [PATCH 54/54] =?UTF-8?q?fix=20=F0=9F=90=9B:=20don't=20reverse=20y?=
 =?UTF-8?q?Scale?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/charts/bar/bars-state.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx
index 7cd5e64af..e85ad2b5f 100644
--- a/app/charts/bar/bars-state.tsx
+++ b/app/charts/bar/bars-state.tsx
@@ -132,7 +132,7 @@ const useBarsState = (
       .paddingInner(PADDING_INNER)
       .paddingOuter(PADDING_OUTER);
     const yScaleInteraction = scaleBand()
-      .domain(bandDomain.reverse())
+      .domain(bandDomain)
       .paddingInner(0)
       .paddingOuter(0);
 
@@ -226,7 +226,7 @@ const useBarsState = (
   xScale.range([0, chartWidth]);
   yScaleInteraction.range([0, adjustedHeight]);
   yScaleTimeRange.range([0, adjustedHeight]);
-  yScale.range([adjustedHeight, 0]);
+  yScale.range([0, adjustedHeight]);
 
   const isMobile = useIsMobile();