最近突发奇想,用 3D 的堆叠柱图,作了一个搭积木的小游戏。html
主要思路apache
-
用一个几乎透明的 series-bar3D 铺满整个 grid3D,做为操做区,监听鼠标点击事件、完成堆积木的操做;json
-
用多层数据为 0 的 series-bar3D 放在操做层 bar3D 下方,堆积木时,按照从下向上的顺序,更新其数据 series-bar3D.data(包括数值和样式,即 value 和 itemStyle);api
-
用一个 series-heatmap 制做菜单,也是监听鼠标点击事件,实现撤销、重作、重置、修改积木样式(高度、颜色和透明度)等功能。数组
效果演示微信
家里的笔记本屏幕小,菜单按钮上的文字几乎全都显示不全了……app
关键代码echarts
-
生成数据的部分函数
generateData = (length) => { let ret = { x: [], y: [], boxWidth: length, boxDepth: length, boxHeight: length, operatingSeriesData: [], brickSeriesData: [] }; let brickSeriesDataItem = []; for (let i = 0; i < length; i++) { ret.x.push('x_' + i); ret.y.push('y_' + i); for (let j = 0; j < length; j++) { ret.operatingSeriesData.push([i, j, 1]); brickSeriesDataItem.push({ value: [i, j, 0] }); } } for (let i = 0; i < length; i++) { ret.brickSeriesData[i] = JSON.parse(JSON.stringify(brickSeriesDataItem)); } return ret;};
柱状图堆叠,相同 stack 值的柱状图系列数据会有叠加。注意不一样系列须要叠加的数据项在数组中的索引必须是同样的。优化
https://echarts.apache.org/zh/option-gl.html#series-bar3D.stack
因为一开始对 3D 堆叠柱图的堆叠机制了解不够深刻(自觉得是,没仔细看配置项手册,你们不要学我哈- -),因此一上来就把全部可能用到的砖块数据都生成出来了……也无论最终是否会用到。这里还有优化的空间……
-
series-heatmap.data 生成部分
generateMenuData = (colorList, sizeList) => { let ret = []; for (let i = 0; i < sizeList.length; i++) { ret.push({ value: [i, 1, sizeList[i]], name: 'size', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }); } for (let i = 0; i < colorList.length + 1; i++) { if (i === colorList.length) { ret.push({ value: [i, 0, 1], name: 'empty', label: { show: true, color: 'black' }, itemStyle: { color: '#FFF', opacity: 0.1 } }); continue; } ret.push({ value: [i, 0, 1], name: 'color', label: { show: true, color: 'black' }, itemStyle: { color: colorList[i] } }); } ret.push({ value: [0, 2, 1], name: 'undo', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }, { value: [1, 2, 1], name: 'redo', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }, { value: [2, 2, 1], name: 'reset', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }, { value: [3, 2, 1], name: 'save', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }, { value: [4, 2, 1], name: 'load', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }); return ret;};
-
option.series 生成
generateSeries = (src) => { ret = []; for (let i = 0; i < src.boxHeight; i++) { ret.push({ type: 'bar3D', name: 'bricks', color: 'LawnGreen', data: src.brickSeriesData[i], bevelSize: i === 0 ? 0 : 0.2, bevelSmoothness: i === 0 ? 0 : 2, barSize: [1, 1], stack: 'stack', silent: true, shading: 'lambert', itemStyle: { opacity: i === 0? 1: 0 } }); } ret.push({ type: 'bar3D', name: 'operatingSeries', data: src.operatingSeriesData, barSize: [1, 1], stack: 'stack', color: '#FFA', shading: 'lambert', label: { emphasis: { show: false } }, itemStyle: { opacity: 0.01 }, emphasis: { itemStyle: { opacity: 1 } } }); ret.push({ type: 'heatmap', name: 'menu', tooltip: { formatter: params => { if (params.name === 'color') { return `点击更换“积木”颜色为 ${params.color}`; } if (params.name === 'size') { return `点击更换“积木”高度为 ${params.value[2]}`; } return {undo: '撤销', redo: '重作', reset: '清空', save: '导出游戏数据,<br />供下次赋值给 loadData 使用', load: '功能开发中…' }[params.name]; } }, label: { normal:{ formatter: params => { if (params.name === 'color') { return params.color; } if (params.name === 'size') { return params.value[2]; } return params.name; } } }, itemStyle: { borderColor: '#AAA', borderWidth: 4 }, data: generateMenuData(menuConfig.colorList, menuConfig.sizeList) }); return ret;};
经过 tooltip.formatter 和 label.normal.formatter 定义按钮的文字和提示框内容
-
撤销、重作函数定义
// 撤销undo = () => { if (history.undoList.length === 0) { alert('操做历史记录为空,撤销未执行…'); return console.log('操做历史记录为空,撤销未执行…'); } // undoList 最后一条记录“剪切”到 redoList let historyObj = history.undoList.pop(); history.redoList.push(historyObj); // 将上一步操做/重作的 series[seriesIndex].data[dataIndex] 重置为初始值 let val = series[historyObj.seriesIndex].data[historyObj.dataIndex].value; val[2] = 0; series[historyObj.seriesIndex].data[historyObj.dataIndex] = {value: val}; myChart.setOption({series: series}); console.log('撤销成功');};// 重作redo = () => { if (history.redoList.length === 0) { alert('操做历史记录为空,重作未执行…'); return console.log('操做历史记录为空,重作未执行…'); } // redoList 最后一条记录“剪切”到 undoList let historyObj = history.redoList.pop(); history.undoList.push(historyObj); // 将上一步重置的 series[seriesIndex].data[dataIndex] 重设为撤销前的状态 series[historyObj.seriesIndex].data[historyObj.dataIndex].value[2] = historyObj.brickConfig.size; series[historyObj.seriesIndex].data[historyObj.dataIndex].itemStyle = { color: historyObj.brickConfig.color, opacity: historyObj.brickConfig.opacity }; myChart.setOption({series: series}); console.log('重作成功');};// 撤销/重作 所用的操做历史记录let history = { undoList: [], redoList: []};
-
鼠标单击事件监听处理
// 监听鼠标点击事件myChart.on('click', params => { // 菜单操做处理 if (params.seriesName === 'menu') { if (params.name === 'color') { brickConfig.color = params.color; brickConfig.opacity = 1; myChart.setOption({title:{subtext: `当前颜色:${brickConfig.color}\n当前尺寸:${brickConfig.size}\n当前透明度:${brickConfig.opacity}`}}); return console.log(`砖块颜色更换为${params.color}`); } if (params.name === 'empty') { brickConfig.color = params.color; brickConfig.opacity = 0; myChart.setOption({title:{subtext: `当前颜色:${brickConfig.color}\n当前尺寸:${brickConfig.size}\n当前透明度:${brickConfig.opacity}`}}); return console.log(`砖块颜色更换为透明`); } if (params.name === 'size') { brickConfig.size = params.value[2]; myChart.setOption({title:{subtext: `当前颜色:${brickConfig.color}\n当前尺寸:${brickConfig.size}\n当前透明度:${brickConfig.opacity}`}}); return console.log(`砖块 size 更换为${params.value[2]}`); } if (params.name === 'load') { // load alert('开发中…'); return console.log('开发中…'); } if (params.name === 'reset') { data = generateData(xLength); series = generateSeries(data); myChart.setOption({series: series}); return console.log('清空数据成功'); } if (params.name === 'save') { let uri = 'data:application/json;base64,'; //console.log(data); window.location.href = uri + base64(JSON.stringify(data)); return console.log('导出数据成功'); } if (params.name === 'undo') { return undo(); } if (params.name === 'redo') { return redo(); } } //alert(`正在 (${params.data[0]}, ${params.data[1]}) 处堆积一个砖块`); // 堆积木(砖块)操做处理 for (let i in series) { if (series[i].name === 'bricks' && series[i].data[params.data[0] * xLength + params.data[1]].value[2] === 0) { series[i].data[params.data[0] * xLength + params.data[1]].value[2] = brickConfig.size; series[i].data[params.data[0] * xLength + params.data[1]].itemStyle = { color: brickConfig.color, opacity: brickConfig.opacity }; history.undoList.push({ seriesIndex: i, dataIndex: params.data[0] * xLength + params.data[1], brickConfig: JSON.parse(JSON.stringify(brickConfig)) // 深拷贝 }); history.redoList = []; return myChart.setOption({ series: series }); } }});
主要就是经过 echartsInstance.on 绑定事件处理函数,也就是 myChart.on('click', function(){}) 的形式。
👇阅读原文查看 ECharts Gallery 例子,强烈建议 PC 查看
本文分享自微信公众号 - ZXand618的ECharts之旅(ZXand618)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。