Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Multi-level drill-down #16152

Open
dirslashls opened this issue Dec 1, 2021 · 12 comments
Open

[Feature] Multi-level drill-down #16152

dirslashls opened this issue Dec 1, 2021 · 12 comments
Labels
en This issue is in English new-feature
Milestone

Comments

@dirslashls
Copy link

What problem does this feature solve?

The 5.2.0 feature of Universal Transition works great for drilling down 1 level. I am trying to create drilling down to multi-levels and can't figure out how to do it. My idea was that the groupId from the previous level becomes dataGroupId of the current level and the current level will use the itemGroupId for the groups of the current dataset. However, specifying itemGroupId in the second level completely stopped the animation.

What does the proposed API look like?

I think there should be a way to distinguish between the group a data item belongs to from the drill-through group and also a group it forms for further drill-down.

@echarts-bot echarts-bot bot added en This issue is in English pending We are not sure about whether this is a bug/new feature. waiting-for: community labels Dec 1, 2021
@pissang pissang added the missing-demo The author should provide a demo. label Dec 1, 2021
@echarts-bot
Copy link

echarts-bot bot commented Dec 1, 2021

@dirslashls
Copy link
Author

Hello please use the following code which is a slight modification of the bar chart animation at https://echarts.apache.org/examples/en/editor.html?c=bar-drilldown. That one has 2 levels and I am trying to create 3 levels.

In the below code, I have an option indicating commenting "groupId" shows the universal transition from level 1 to 2. But drilling level 2 to 3 will not be possible as I won't know the level 3 grouping from level 2 data. To support multi-level drill-down I think there needs to be two group ids. One about the group a data item belongs to w.r.t the parent and one about the group the child data elements belong to.

option = {
  xAxis: {
    data: ['Animals', 'Fruits', 'Cars']
  },
  yAxis: {},
  dataGroupId: '',
  animationDurationUpdate: 500,
  series: {
    type: 'bar',
    id: 'sales',
    data: [
      {
        value: 5,
        groupId: 'animals'
      },
      {
        value: 2,
        groupId: 'fruits'
      },
      {
        value: 4,
        groupId: 'cars'
      }
    ],
    universalTransition: {
      enabled: true,
      divideShape: 'clone'
    }
  }
};
const drilldownData = [
  {
    dataGroupId: 'animals',
    data: [
      ['Cats', 4,'household'],
      ['Dogs', 2,'household'],
      ['Cows', 1,'farm'],
      ['Sheep', 2,'farm'],
      ['Pigs', 1,'farm']
    ]
  },
  {
    dataGroupId: 'fruits',
    data: [
      ['Apples', 4,'sweet'],
      ['Oranges', 2,'sour']
    ]
  },
  {
    dataGroupId: 'cars',
    data: [
      ['Toyota', 4,'japanese'],
      ['Opel', 2,'german'],
      ['Volkswagen', 2,'german']
    ]
  }
];

const drilldownData2 = [
  { dataGroupId: 'household',
    data: [['Canine',2],['Feline',1]]
  },
]

let level = 0;
myChart.on('click', function (event) {
  level = (level+1)%3;
  console.debug(level);
  if (event.data) {
    var subData = (level === 1 ? drilldownData : drilldownData2).find(function (data) {
      return data.dataGroupId === event.data.groupId;
    });
    if (!subData) {
      return;
    }
    myChart.setOption({
      xAxis: {
        data: subData.data.map(function (item) {
          return item[0];
        })
      },
      series: {
        type: 'bar',
        id: 'sales',
        dataGroupId: subData.dataGroupId,
        data: subData.data.map(function (item) {
          return { value: item[1] , 
               groupId: item[2] /* comment groupId and the universal transition works but not 2nd level drill */ 
          };
        }),
        universalTransition: {
          enabled: true,
          divideShape: 'clone'
        }
      },
      graphic: [
        {
          type: 'text',
          left: 50,
          top: 20,
          style: {
            text: 'Back',
            fontSize: 18
          },
          onclick: function () {
            level = -1;
            myChart.setOption(option);
          }
        }
      ]
    });
  }
});

@pissang pissang removed pending We are not sure about whether this is a bug/new feature. waiting-for: community missing-demo The author should provide a demo. labels Dec 2, 2021
@pissang pissang added this to the 5.4 milestone Dec 2, 2021
@tyn1998
Copy link
Contributor

tyn1998 commented May 28, 2022

Hi, @dirslashls, it is the second time I commenting on this issue. I commented here 3 days ago but I found I was wrong, so I deleted those wrong info and reworked with this problem today.

Well, 3 days ago, I posted this gif and I thought I successfully did it:

Apparently it was not what you want. What you request for is this:

I used a very dirty way to achieve this. If you are interested in this workaround, I will put it in another comment.

@dirslashls
Copy link
Author

Hi @tyn1998 , yes, this new version is what I am looking for. Thanks for figuring out a way to achieve it. I am interested in the write-up of how to do it.

@tyn1998
Copy link
Contributor

tyn1998 commented May 29, 2022

Hi @dirslashls, below is the snippet. You can run it in online editor to have a try:

// level 1 (root)
const data_1 = {
  dataGroupId: '1',
  data: [
    ['1_1', 5, '1_1'], // x, y, groupId
    ['1_2', 2, '1_2']
  ]
};

// level 2
const data_1_1 = {
  dataGroupId: '1_1',
  data: [
    ['1_1_1', 2, '1_1_1'],
    ['1_1_2', 2, '1_1_2'],
    ['1_1_3', 3, '1_1_3']
  ]
};

const data_1_2 = {
  dataGroupId: '1_2',
  data: [
    ['1_2_1', 6, '1_2_1'],
    ['1_2_2', 7, '1_2_2']
  ]
};

// level 3
const data_1_1_1 = {
  dataGroupId: '1_1_1',
  data: [
    ['1_1_1_A', 5],
    ['1_1_1_B', 6],
    ['1_1_1_C', 7],
    ['1_1_1_D', 8]
  ]
};

const data_1_1_2 = {
  dataGroupId: '1_1_2',
  data: [
    ['1_1_2_A', 2],
    ['1_1_2_B', 9]
  ]
};

const data_1_1_3 = {
  dataGroupId: '1_1_3',
  data: [
    ['1_1_3_A', 1],
    ['1_1_3_B', 2],
    ['1_1_3_C', 8]
  ]
};

const data_1_2_1 = {
  dataGroupId: '1_2_1',
  data: [
    ['1_2_1_A', 9],
    ['1_2_1_B', 2],
    ['1_2_1_C', 1]
  ]
};

const data_1_2_2 = {
  dataGroupId: '1_2_2',
  data: [
    ['1_2_2_A', 7],
    ['1_2_2_B', 7]
  ]
};

const allDataGroups = [
  data_1,
  data_1_1,
  data_1_2,
  data_1_1_1,
  data_1_1_2,
  data_1_1_3,
  data_1_2_1,
  data_1_2_2
];

// Generate 1+1 options for each data
const allOptionsWithItemGroupId = {};
const allOptionsWithoutItemGroupId = {};

allDataGroups.forEach((dataGroup, index) => {
  const { dataGroupId, data } = dataGroup;
  const optionWithItemGroupId = {
    xAxis: {
      type: 'category'
    },
    yAxis: {},
    // dataGroupId: dataGroupId,
    animationDurationUpdate: 500,
    series: {
      type: 'bar',
      // id: "sales",
      dataGroupId: dataGroupId,
      encode: {
        x: 0,
        y: 1,
        itemGroupId: 2
      },
      data: data,
      universalTransition: {
        enabled: true,
        divideShape: 'clone'
      }
    },
    graphic: [
      {
        type: 'text',
        left: 50,
        top: 20,
        style: {
          text: 'Back',
          fontSize: 18
        },
        onclick: function () {
          goBack();
        }
      }
    ]
  };
  const optionWithoutItemGroupId = {
    xAxis: {
      type: 'category'
    },
    yAxis: {},
    // dataGroupId: dataGroupId,
    animationDurationUpdate: 500,
    series: {
      type: 'bar',
      // id: "sales",
      dataGroupId: dataGroupId,
      encode: {
        x: 0,
        y: 1
        // itemGroupId: 2,
      },
      data: data.map((item, index) => {
        return item.slice(0, 2);  // This is what "without itemGroupId" means
      }),
      universalTransition: {
        enabled: true,
        divideShape: 'clone'
      }
    },
    graphic: [
      {
        type: 'text',
        left: 50,
        top: 20,
        style: {
          text: 'Back',
          fontSize: 18
        },
        onclick: function () {
          goBack();
        }
      }
    ]
  };
  allOptionsWithItemGroupId[dataGroupId] = optionWithItemGroupId;
  allOptionsWithoutItemGroupId[dataGroupId] = optionWithoutItemGroupId;
});

// A stack to remember previous dataGroupsId
const dataGroupIdStack = [];

const goForward = (dataGroupId) => {
  dataGroupIdStack.push(myChart.getOption().series[0].dataGroupId); // push current dataGroupId into stack.
  myChart.setOption(allOptionsWithoutItemGroupId[dataGroupId], false);
  myChart.setOption(allOptionsWithItemGroupId[dataGroupId], false);  // setOption twice? Yeah, it is dirty.
};

const goBack = () => {
  if (dataGroupIdStack.length === 0) {
    console.log('Already in root dataGroup!');
  } else {
    console.log('Go back to previous level');
    myChart.setOption(
      allOptionsWithoutItemGroupId[myChart.getOption().series[0].dataGroupId],
      false
    );
    myChart.setOption(allOptionsWithItemGroupId[dataGroupIdStack.pop()], true); // Note: the parameter notMerge is set true
  }
};

option = allOptionsWithItemGroupId['1'];  // The initial option is the root data option

myChart.on('click', 'series.bar', (params) => {
  if (params.data[2]) {  // If current params is not belong to the "childest" data, then it has data[2]
    const dataGroupId = params.data[2];
    goForward(dataGroupId);
  }
});

@dirslashls
Copy link
Author

Thanks @tyn1998 , it is a clever hack. I think if dataGroupId in the options can be optionally an array to track the stack, then this hack won't be required.

Hello @pissang , do you suggest going with the hack or would there be an actual fix for this?

@tyn1998
Copy link
Contributor

tyn1998 commented May 29, 2022

Actually this issue is linked to one project for OSPP Summer2022. I would like to apply for this project, good luck to me! 😄

@tyn1998
Copy link
Contributor

tyn1998 commented Aug 6, 2022

[8.6] A Possible Interface for This Feature

Hi, guys!

I have come up with an interface to allow users to implement multi-level drill-down/up easily and intuitively. The new interface will be compatible with the old way of using groupId to trigger a universalTransition, which means the new design is not a breaking change.

The possible new interface: childGroupId

Good news! There is only a single new key introduced to the new interface design and that is childGroupId. We alreay have groupId, right? A groupId can decide which group the dataItem belongs to, and if no groupId is specified, echarts will use series.dataGroupId as the dataItem's groupId. If 1/N oldDataItem's groupIdis matched with 1/N newDataItem's groupId and with universalTransition enabled in both sides, the universalTransition will happen.

Now, with childGroupId, you can specify it in dataItems to introduce a real hierarchical relationship. As the three hierarchical data show below, if a logical parent's childGroupId is equal to his/her logical child's groupId, they are regarded as being matched.

In a set of hierarchical datas, the items' groupIds of the root data can be whatever or undefined and the items' childGroupIds of the "childest" data can be ignored too. The rest dataItems are linked logically by many pairs of childGroupId and groupId.

root child grandChild
image.png image.png image.png

I mention the word "logical" several times to stress that the hierarchical relationship is only in our brain, for echarts, however, it does not identify the hierarchical relationship directly. It only see an old option and a new option and that is a hurdle I should clear first when I try to implement the feature.

How to match childGroupId with groupId in code?

parent2child child2parent
image.png image.png

Assume that we are setting option from root to child. We know the old option is a "parent" and the new option is a "child", so we will compare oldDataItem.childGroupId with newDataItem.groupId. If they are equal, they are matched. What if we set option from child to root? Yes, we will compare oldDataItem.groupId to newDataItem.childGroupId to check if they are metched.

It is so natural since we have a context of the hierarchical relationship in our mind. But echarts has no idea about which two keys it should compare because it does not know whether it is a "parent2child" setOption or a "child2parent" setOption.

The solution I figured out is to decide the direction ("parent2child" or "child2parent") before createKeyGetter(), so in createKeyGetter() I can return the right key accordingly. For example:

  • if isOld === true and direction === 'parent2child', the returned key should be the value of childGroupId.
  • if isOld === true and direction === 'child2parent', the returned key should be the value of groupId.
  • if direction === 'nodirection', we fallback to the case that only using groupId as the key and this is the reason why there is no breaking change introduced.

Then, how I decide the direction? I will explain it later since it is too detailed.

Others

Please review my design for the interface of the new feature and if anything is wrong, feel free to correct me, thanks!

@Ovilia
Copy link
Contributor

Ovilia commented Aug 12, 2022

@tyn1998 Thanks for the detailed plan. I have some questions about this:

If we consider drilling-down only, then all groupIds the data items should be the same because typically we can only drill one thing down. But what happens if there are different groupIds in the new option?

If I understand correctly, you want the developers to listen to all kinds of click events and call setOption again like this, right?

chart.setOption(/* old option */);
chart.on('click', () => {
  // check which item is clicked and find its children
  chart.setOption(/* new option with its children */);
})

One problem is that, in this design, the developer has to consider about all possible drilled down situations in the click callback and handle the corresponding new option.

I think a better idea would be encapsulating the logic by ECharts like:

chart.setOption({
  series: [{
    // ...
    data: [{
      value: 10,
      name: 'parent A',
      drilldown: {
        data: [{
          value: 2,
          name: 'first child of parent A'
        }],
        // other options for drilldown if you think is needed
      }
    }]
  }]
});

So the developers don't have to think about drilling down at all. Apache ECharts will update the chart just as it does with treemap drilldown right now.

That's my personal point of view. Let's also hear about what other mentors think.

@JerichoBarquez
Copy link

JerichoBarquez commented Mar 14, 2023

Hi @tyn1998 , thank you for sharing this. But I need your help as i was trying to create a chart with Series data. Please see sample photo
image

but when i clicked 1 bar for example the green bar, the 1st photo is still in the next graph
image

document.addEventListener("DOMContentLoaded", () => {
var columnChart = echarts.init(document.getElementById('columnChart'));

                    var emphasisStyle = {
                      itemStyle: {
                        shadowBlur: 10,
                        shadowColor: 'rgba(0,0,0,0.3)'
                      }
                    };
                    

                     const option = {
                      tooltip: {},
                      toolbox: {
                        feature: {
                          dataView: { show: true, readOnly: false },
                          magicType: { show: true, type: ['line', 'bar'] },
                          restore: { show: true },
                          saveAsImage: { show: true }
                        }
                      },
                      
                      legend: {
                        data: ['APAC', 'EMEA', 'USCA']
                      },
                      xAxis: [
                        {
                          type: 'category',
                          data: ['January', 'February'],
                          axisPointer: {
                            type: 'shadow'
                          }
                        }
                      ],
                      yAxis: [
                        {
                          type: 'value',
                          // name: 'Precipitation',
                          min: 0,
                          max: 250,
                          interval: 50,
                          axisLabel: {
                            formatter: '{value}'
                          }
                        }
                        
                      ],
                      series: [
                        {
                          id : 'APAC',
                          name: 'APAC',
                          type: 'bar',
                          color: '#00A3E0',
                          emphasis: emphasisStyle,
                          tooltip: {
                            valueFormatter: function (value) {
                              return value;
                            }
                          },
                          data: [
                            135.6
                          ]
                        },
                        {
                          id : 'EMEA',
                          name: 'EMEA',
                          type: 'bar',
                          color: '#2eca6a',
                          emphasis: emphasisStyle,
                          tooltip: {
                            valueFormatter: function (value) {
                              return value;
                            }
                          },
                          data: [
                            182.2
                          ]
                        },
                        {
                          id : 'USCA',
                          name: 'USCA',
                          type: 'bar',
                          color: 'orange',
                          emphasis: emphasisStyle,
                          tooltip: {
                            valueFormatter: function (value) {
                              return value;
                            }
                          },
                          data: [
                            120.6
                          ]
                        }
                      ]
                    };


                    const drilldownData = [
                      {
                        dataGroupId: 'APAC',
                        name: 'APAC',
                        data: [
                          ['XX', 47],
                          ['YY', 29],
                          ['AA', 61],
                          ['BB', 29]
                        ]
                      },
                      {
                        dataGroupId: 'EMEA',
                        name: 'EMEA',
                        data: [
                          ['XX', 54],
                          ['YY', 72],
                          ['AA', 41],
                          ['BB', 62]
                        ]
                      },
                      {
                        dataGroupId: 'USCA',
                        name: 'USCA',
                        data: [
                          ['XX', 10],
                          ['YY',12],
                          ['AA', 18],
                          ['BB', 27]
                        ]
                      }
                    ];

                  columnChart.on('click', 'series.bar', function (event) {
                    
                    if (event.data) {
                      var subData = drilldownData.find(function (data) {
                        return data.dataGroupId === event.seriesId;
                      });
                      if (!subData) {
                        return;
                      }
                      // console.log(subData)
                      columnChart.setOption({
                        xAxis: {
                          data: subData.data.map(function (item) {
                            // console.log(columnChart.getOption())
                            return item[0];
                          })
                        },
                        legend: {
                          data: [subData.dataGroupId]
                        },
                        series: [
                          {
                            name: subData.dataGroupId,
                            id: subData.dataGroupId,
                            type: 'bar',
                            dataGroupId: subData.dataGroupId,
                            data: subData.data.map(function (item) {
                              return item[1];
                            }),
                            universalTransition: {
                              enabled: true,
                              divideShape: 'clone'
                            },
                          }
                        ],
                        graphic: [
                          {
                            type: 'text',
                            left: 50,
                            top: 20,
                            style: {
                              text: 'Back',
                              fontSize: 18
                            },
                            onclick: function () {
                              
                              columnChart.setOption(option);
                              
                             
                            }
                          }
                        ]
                      });
                      
                    }
                  });
                    
                    option && columnChart.setOption(option);
                    // console.log(columnChart)
                  });

hope you could check on this. My goal is when i click the green bar - only the green bar will appear in the next graph. the blue and orange should not appear. TIA

@tyn1998
Copy link
Contributor

tyn1998 commented Mar 14, 2023

Hi @JerichoBarquez, I tried to read your code but could not understand it well. Maybe you would like to try the workaround provided in #16152 (comment). And especially pay attention to notMerge option of instance.setOption()

BTW, I added multi-level drill-down support to the codebase last summer #17611 and the feature is supposed to be released with the version 5.5.0. You may want for a try then :)

@JerichoBarquez
Copy link

JerichoBarquez commented Mar 17, 2023

Hi @tyn1998
Tried your suggestions but i did not work in my side.
I used the code here but i made some twist:
https://echarts.apache.org/examples/en/editor.html?c=bar-drilldown

Here what it looks like:
image

I want to achieve the drill down similar to this after clicking one bar under the month of January but without the green and orange bars.
image

Thanks in advance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
en This issue is in English new-feature
Projects
None yet
Development

No branches or pull requests

5 participants