GithubHelp home page GithubHelp logo

Comments (5)

Justineo avatar Justineo commented on June 7, 2024 1

As you can see from the release log, v6.5.3 changed the behavior of the internal setOption.

In your case you may need to apply :update-options="{ notMerge: false }" to your chart component. Otherwise when you update the option upon legendselectchanged, ECharts may consider that you are creating a new chart and the instance may lose its internal legend selection state.

from vue-echarts.

Justineo avatar Justineo commented on June 7, 2024 1

You are creating a new option object in the computed function:

https://github.com/BenJackGill/vue-echarts-bad/blob/6151073f30ffc0a4010492de998bfbf59dbafed9/src/App.vue#L133-L151

So barChartOptions.value will be a fresh object each time its dependencies change. The legendselectchanged event is irrelevant here.

from vue-echarts.

BenJackGill avatar BenJackGill commented on June 7, 2024

Thank you. Your fix worked.

I have read over this part from the docs again:

When update-options is not specified, notMerge: false will be specified by default when the setOption method is called if the option object is modified directly and the reference remains unchanged; otherwise, if a new reference is bound to option, notMerge: true will be specified.

But I'm confused why I need to specify :update-options="{ notMerge: false }". In this case shouldn't notMerge: false be the default setting?

The legendselectchanged event is used to trigger a change in the option object. The computed reference remains unchanged. Therefore notMerge: false should be used by default.

from vue-echarts.

BenJackGill avatar BenJackGill commented on June 7, 2024

Ok thanks I understand now. Thank you for taking the time to help explain this to me :)

from vue-echarts.

BenJackGill avatar BenJackGill commented on June 7, 2024

Posting this info here for my own benefit when I have a similar problem in the future.

The crux of the issue is that my chart data loads async. Because of this I thought using a Computed Ref instead of a plain Ref to build the options object would be a good idea. But the Computed Ref object had some properties that were reactive variables, and whenever those reactive properties changed the Computed Ref recomputation was creating a new object reference (news to me at the time!). Therefore we need to add :update-options="{ notMerge: false }" to ensure the new object gets merged with the old object.

Another solution would be to refactor and go back to using a plain Ref with a static options object. Then use watchers and such to update the object properties in place (barChartOptions.value.series = newSeriesData). In scenario we do not need :update-options="{ notMerge: false }" because are not creating a new object reference each time. We are just changing the object properties in place, and therefore notMerge: false will already be used by default.

For completeness sake I will post a couple version of the full SFC here, but a lot of the logic for rounding the stacked bar chart is not needed. That's just what I had with the initial issue before learning the real problem.

Here is my old problematic code from the original issue:

Note, this version can easily be fixed by adding update-options="{ notMerge: false }" to the v-chart.

<template>
  <v-chart
    class="echart-container"
    autoresize
    :option="barChartOptions"
    @legendselectchanged="handleLegendSelectChanged"
  />
</template>

<script setup>
import { computed, onMounted, ref } from "vue";
import { use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { BarChart } from "echarts/charts";
import { LegendComponent, GridComponent } from "echarts/components";
import VChart from "vue-echarts";

use([GridComponent, LegendComponent, BarChart, CanvasRenderer]);

// Create some fake async data
const asyncData = ref([]);

onMounted(async () => {
  // Pause for fake async load
  await new Promise((resolve) => setTimeout(resolve, 500));
  // Load the data
  asyncData.value = [
    {
      date: new Date("2023-11-22T17:00:00.000Z"),
      appearances: 1,
      missedOpportunities: 2,
    },
    {
      date: new Date("2023-11-23T17:00:00.000Z"),
      appearances: 2,
      missedOpportunities: 1,
    },
  ];
});

// Track series visibility
const seriesVisibility = ref({
  "Missed Opportunities": true,
  Appearances: true,
});

// Update series visibility when legend is toggled
const handleLegendSelectChanged = (legend) => {
  Object.entries(legend.selected).forEach(([selectedKey, selectedValue]) => {
    seriesVisibility.value[selectedKey] = selectedValue;
  });
};

// Create computed options for the chart
const barChartOptions = computed(() => {
  // Create base series (data for this is added later)
  const baseSeries = [
    {
      name: "Appearances",
      type: "bar",
      color: "#FF0000",
      stack: "ranks",
    },
    {
      name: "Missed Opportunities",
      type: "bar",
      color: "#333333",
      stack: "ranks",
    },
  ];

  // Function to get the top stacked series for each date
  const getTopSeriesForEachDate = () => {
    // Object to store top series name for each date
    const topSeriesForEachDate = {};

    asyncData.value.forEach((dataPoint) => {
      let topSeriesName = "";
      // Check which series is on top for this data point
      if (
        seriesVisibility.value["Missed Opportunities"] &&
        dataPoint.missedOpportunities > 0
      ) {
        topSeriesName = "Missed Opportunities";
      } else if (
        seriesVisibility.value["Appearances"] &&
        dataPoint.appearances > 0
      ) {
        topSeriesName = "Appearances";
      }
      // Store the top series name for this date
      if (topSeriesName) {
        topSeriesForEachDate[dataPoint.date.toDateString()] = topSeriesName;
      }
    });

    return topSeriesForEachDate;
  };

  // Function to add border radius to the top stacked series
  const getSeriesDataWithTopStackBorderRadius = (stackInfo) => {
    // Iterate over base series and create a new series
    const series = baseSeries.map((seriesItem) => {
      // Iterate over asyncData and create a new array of series data
      const seriesData = asyncData.value.map((dataPoint) => {
        const dataPointDateString = dataPoint.date.toDateString();
        const dataPointTopStackName = stackInfo[dataPointDateString];
        const isTopStack = dataPointTopStackName === seriesItem.name;

        // Return the data item with the border radius applied
        return {
          value: [
            dataPoint.date,
            seriesItem.name === "Appearances"
              ? dataPoint.appearances
              : dataPoint.missedOpportunities,
          ],
          itemStyle: {
            borderRadius: isTopStack ? [20, 20, 0, 0] : [0, 0, 0, 0],
          },
        };
      });

      const seriesOption = {
        ...seriesItem,
        data: seriesData,
      };
      return seriesOption;
    });

    return series;
  };

  // Get the new series data with the top stack border radius applied
  const seriesWithTopStackBorderRadius = getSeriesDataWithTopStackBorderRadius(
    getTopSeriesForEachDate()
  );

  // Return the options object
  const options = {
    xAxis: {
      type: "time",
      axisLabel: {
        formatter: "{d} {MMM} {yy}",
      },
      minInterval: 3600 * 1000 * 24, // 1 day in milliseconds
    },
    yAxis: {
      type: "value",
      show: true,
      minInterval: 1,
    },
    series: seriesWithTopStackBorderRadius,
    legend: {
      show: true,
    },
  };
  return options;
});
</script>

<style scoped>
.echart-container {
  height: 500px;
  width: 500px;
}
</style>

Here is an alternate version that uses a plain Ref with a static object and watchers to update the object properties in place:

<template>
  <v-chart
    class="echart-container"
    autoresize
    :option="barChartOptions"
    @legendselectchanged="handleLegendSelectChanged"
  />
</template>

<script setup>
import { onMounted, ref, watch } from "vue";
import { use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { BarChart } from "echarts/charts";
import { LegendComponent, GridComponent } from "echarts/components";
import VChart from "vue-echarts";

use([GridComponent, LegendComponent, BarChart, CanvasRenderer]);

// Create some fake async data
const asyncData = ref([]);

onMounted(async () => {
  // Pause for fake async load
  await new Promise((resolve) => setTimeout(resolve, 500));
  // Load the async data
  asyncData.value = [
    {
      date: new Date("2023-11-22T17:00:00.000Z"),
      appearances: 1,
      missedOpportunities: 2,
    },
    {
      date: new Date("2023-11-23T17:00:00.000Z"),
      appearances: 2,
      missedOpportunities: 1,
    },
  ];
  // Update the chart options
  const seriesWithTopStackBorderRadius = getSeriesDataWithTopStackBorderRadius(
    getTopSeriesForEachDate()
  );
  barChartOptions.value.series = seriesWithTopStackBorderRadius;
});

// Create base series (data for each series will be added later)
const baseSeries = [
  {
    name: "Appearances",
    type: "bar",
    color: "#FF0000",
    stack: "ranks",
  },
  {
    name: "Missed Opportunities",
    type: "bar",
    color: "#333333",
    stack: "ranks",
  },
];

// Track series visibility
const seriesVisibility = ref({
  "Missed Opportunities": true,
  Appearances: true,
});

// Update series visibility when legend is toggled
const handleLegendSelectChanged = (legend) => {
  Object.entries(legend.selected).forEach(([selectedKey, selectedValue]) => {
    seriesVisibility.value[selectedKey] = selectedValue;
  });
};

// Function to get the top stacked series for each date
const getTopSeriesForEachDate = () => {
  // Object to store top series name for each date
  const topSeriesForEachDate = {};

  asyncData.value.forEach((dataPoint) => {
    let topSeriesName = "";
    // Check which series is on top for this data point
    if (
      seriesVisibility.value["Missed Opportunities"] &&
      dataPoint.missedOpportunities > 0
    ) {
      topSeriesName = "Missed Opportunities";
    } else if (
      seriesVisibility.value["Appearances"] &&
      dataPoint.appearances > 0
    ) {
      topSeriesName = "Appearances";
    }
    // Store the top series name for this date
    if (topSeriesName) {
      topSeriesForEachDate[dataPoint.date.toDateString()] = topSeriesName;
    }
  });

  return topSeriesForEachDate;
};

// Function to add border radius to the top stacked series
const getSeriesDataWithTopStackBorderRadius = (stackInfo) => {
  // Iterate over base series and create a new series
  const series = baseSeries.map((seriesItem) => {
    // Iterate over asyncData and create a new array of series data
    const seriesData = asyncData.value.map((dataPoint) => {
      const dataPointDateString = dataPoint.date.toDateString();
      const dataPointTopStackName = stackInfo[dataPointDateString];
      const isTopStack = dataPointTopStackName === seriesItem.name;

      // Return the data item with the border radius applied
      return {
        value: [
          dataPoint.date,
          seriesItem.name === "Appearances"
            ? dataPoint.appearances
            : dataPoint.missedOpportunities,
        ],
        itemStyle: {
          borderRadius: isTopStack ? [20, 20, 0, 0] : [0, 0, 0, 0],
        },
      };
    });

    const seriesOption = {
      ...seriesItem,
      data: seriesData,
    };
    return seriesOption;
  });

  return series;
};

// Watch for changes to asyncData and update the chart option properties in place
watch(
  seriesVisibility,
  () => {
    console.log("seriesVisibility changed");
    const seriesWithTopStackBorderRadius =
      getSeriesDataWithTopStackBorderRadius(getTopSeriesForEachDate());
    barChartOptions.value.series = seriesWithTopStackBorderRadius;
  },
  { deep: true }
);

// Create computed options for the chart
const barChartOptions = ref({
  xAxis: {
    type: "time",
    axisLabel: {
      formatter: "{d} {MMM} {yy}",
    },
    minInterval: 3600 * 1000 * 24, // 1 day in milliseconds
  },
  yAxis: {
    type: "value",
    show: true,
    minInterval: 1,
  },
  series: [], // Initial series data is empty, and will be added later
  legend: {
    show: true,
  },
});
</script>

<style scoped>
.echart-container {
  height: 500px;
  width: 500px;
}
</style>

from vue-echarts.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.