文章目录
- 成品展示
- 思路介绍
- 1、如何让柱子不是从0开始
- 2、左侧y轴的时间怎么表示
- 3、柱子上的线怎么画出来
- 做出两个简单柱子
- 下面加上透明柱子
- 合并两个柱子
- y轴左右各一个坐标轴
- y轴固定间隔刻度
- y轴刻度数值转换
- y轴显示标签
- 添加图例
- 对折线图的点样式进行设置
- 显示点上的数字并对数字做转换
- 修改点击后的弹出框
- 完整
成品展示
思路介绍
分析一下这个需求,主要有几个难点:
1、如何让柱子不是从0开始
答:echarts 官方 把这种柱子叫瀑布图,实现的原理就是使用一个透明的柱子给他顶起来。我直接搬过来原文。
2、左侧y轴的时间怎么表示
我本来想到的是 y 轴的 type 设置为 time
但是做着做着,发现不行,我们做瀑布图要用到 stack,而 stack 不支持 type = time
那就只能用 type = value 了,也就是要把时间转化为一个数值,比如在上面的图中,最底部是 13:00,我们先以 13:00 为 0,那么 12:00 就是 1,11:00 就是 2,以此类推。
那么由这个问题又引申出两个小问题:
1、y 轴变为数字了,要将其转化为几点几分的这种时间,这个用 formatter 转换一下就行
2、我们现在是以 13:00 为 0 点,现实应该是以所有数据的最大值为 0 点,并且要取整数,比如最大值是13:30,那么 0 点就是 14:00。这个到时候写一个函数进行单独处理。
3、柱子上的线怎么画出来
这个线代表的夜晚醒来的次数和时间,其实原理还是瀑布图的画法,只不过要均匀的分布在睡眠时长里面,需要用一些算法。
n 代表一共几条线(也就是夜醒几次)
第 x 条线的透明柱子 = 睡眠时长的透明柱子高度 + (睡眠时长柱子高度 / n + 1)* x
第 x 条线的柱子 = 夜醒时长,或者给个固定高度
做出两个简单柱子
option = {xAxis: {type: 'category',data: ['周一', '周二', '周三', '周四', '周五', '周六']},yAxis: {type: 'value'},series: [// 黄色柱子{name: 'bar1',type: 'bar',stack: 'Total1',data: [2900, 1200, 300, 200, 900, 300],itemStyle: {color: '#fac858',barBorderRadius: [8, 8, 0, 0],}},// 紫色柱子{name: 'bar2',type: 'bar',stack: 'Total2',// stackStrategy: 'negative',itemStyle: {color: '#8c7dc2',},data: [2800, 1100, 200, 100, 800, 200]},]
};
下面加上透明柱子
option = {xAxis: {type: 'category',data: ['周一', '周二', '周三', '周四', '周五', '周六']},yAxis: {type: 'value'},series: [// 透明柱子(黄色柱子下面){name: 'Placeholder1',type: 'bar',stack: 'Total1', // 为透明柱子指定不同的 stackstackStrategy: 'negative',itemStyle: {color: 'rgba(0,0,0,0)',},data: [100, 100, 100, 100, 100, 100]},// 黄色柱子{name: 'bar1',type: 'bar',stack: 'Total1',data: [2900, 1200, 300, 200, 900, 300],itemStyle: {color: '#fac858',barBorderRadius: [8, 8, 8, 8],}},// 透明柱子(紫色柱子下面){name: 'Placeholder2',type: 'bar',stack: 'Total2', // 为透明柱子指定不同的 stackstackStrategy: 'negative',itemStyle: {color: 'rgba(0,0,0,0)',},data: [100, 100, 100, 100, 100, 100]},// 紫色柱子{name: 'bar2',type: 'bar',stack: 'Total2',// stackStrategy: 'negative',itemStyle: {color: '#8c7dc2',},data: [2800, 1100, 200, 100, 800, 200]},]
};
合并两个柱子
stack 改为相同
option = {xAxis: {type: 'category',data: ['周一', '周二', '周三', '周四', '周五', '周六']},yAxis: {type: 'value'},series: [// 透明柱子(黄色柱子下面){name: 'Placeholder1',type: 'bar',stack: 'Total', // 为透明柱子指定不同的 stackstackStrategy: 'negative',itemStyle: {color: 'rgba(0,0,0,0)',},data: [100, 100, 100, 100, 100, 100]},// 黄色柱子{name: 'bar1',type: 'bar',stack: 'Total',data: [2900, 1200, 300, 200, 900, 300],itemStyle: {color: '#fac858',barBorderRadius: [8, 8, 8, 8],}},// 透明柱子(紫色柱子下面){name: 'Placeholder2',type: 'bar',stack: 'Total', // 为透明柱子指定不同的 stackstackStrategy: 'negative',itemStyle: {color: 'rgba(0,0,0,0)',},data: [100, 100, 100, 100, 100, 100]},// 紫色柱子{name: 'bar2',type: 'bar',stack: 'Total',itemStyle: {color: '#8c7dc2',},data: [2800, 1100, 200, 100, 800, 200]},]
};
如果把紫色柱子下面的透明柱子的数字改大一点,就是这样
y轴左右各一个坐标轴
当我的图表同时有柱状图和折线图时,我想让左侧y轴显示柱状图的数据,右侧y轴显示折线图的数据。
option = {xAxis: {type: 'category',data: ['周一', '周二', '周三', '周四', '周五', '周六']},yAxis: [{type: 'value', // 左侧 y 轴配置},{type: 'value', // 右侧 y 轴配置},],series: [// 柱状图系列(放在左侧 y 轴上){name: 'bar',type: 'bar',data: [2900, 1200, 300, 200, 900, 300],itemStyle: {color: '#fac858',barBorderRadius: [15, 15, 0, 0],},yAxisIndex: 0, // 指定使用左侧 y 轴},// 折线图系列(放在右侧 y 轴上){name: 'line',type: 'line',data: [2800, 1100, 200, 100, 800, 200],itemStyle: {color: '#8c7dc2',},yAxisIndex: 1, // 指定使用右侧 y 轴},]
};
y轴固定间隔刻度
yAxis: {type: 'value',interval: 100, // 设置刻度的间隔,每隔100一个刻度
}
y轴刻度数值转换
yAxis: {type: 'value',axisLabel: {formatter: function (value) {// 在这里进行刻度值的转化return value + "h";}}
}
y轴显示标签
yAxis: {type: 'value',name: '效率', // 设置 y 轴的名称// y 轴的名称的样式nameTextStyle: {align: 'left'}
}
添加图例
legend: {orient: 'horizontal', // 设置图例的方向为水平bottom: 10, // 设置图例距离底部的距离itemGap: 20, // 设置图例项之间的距离// 这里如果不加,默认会把透明柱状图也展示出来,所以筛选一下data: ['卧床时长', '睡眠时长', '睡眠效率'], // 图例的名称,与 series 中的 name 对应
}
对折线图的点样式进行设置
{name: '睡眠效率',type: 'line',data: [20, 30, 100, 80, 20, 19],// 点为一个圆圈,如果不设置这个,底下的点的颜色会设置不成功symbol: 'circle',// 点的大小symbolSize: 8,lineStyle: {color: '#33b981', // 指定折线的颜色width: 2, // 指定折线的宽度},itemStyle: {color: '#33b981', // 点的填充颜色borderColor: '#fff', // 点的白色边框borderWidth: 2, // 点的边框宽度},yAxisIndex: 1, // 指定使用右侧 y 轴
}
显示点上的数字并对数字做转换
{name: '睡眠效率',type: 'line',data: [20, 30, 100, 80, 20, 19],// 显示点上的数字label: {show: true,position: 'top',formatter: function (params) {// 在这里对数字进行转换return params.value + '%';},},
},
修改点击后的弹出框
tooltip: {textStyle: {fontSize: 14, // 修改文字大小等其他样式},formatter: function (params) {console.log(params)// 在这里对 Tooltip 中的数字进行转换var result = `<span style="color: ${params.color};">${params.seriesName} ${params.data}</span><br>`return result;},
}
完整
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>ECharts</title><head><meta charset="utf-8" /><script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script></head><style></style>
</head><body><div class="test"><div id="main" style="width: 600px;height:600px;"></div></div>
</body>
<script>
// import * as echarts from 'echarts';
var echarts = window.echarts
console.log('测试-', echarts)
var chartDom = document.getElementById('main');
var myChart = echarts.init(chartDom);const polylineColor = '#e74c3c'
const bigBarColor = '#bdc3c7'
const smallBarColor = '#3498db'
const lineColor = '#bdc3c7'/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable no-plusplus */
/* eslint-disable radix */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable max-len */
// @ts-nocheck
// 两个时间相差几个小时
function calculateHours(startTime, endTime) {if (!startTime) return 0;if (!endTime) return 0;// 将时间字符串转换为 Date 对象const startDate = new Date(startTime);const endDate = new Date(endTime);// 计算时间差(毫秒)const timeDifference = endDate - startDate;// 将毫秒转换为分钟并取绝对值const minutesDifference = Math.abs(timeDifference / (1000 * 60));return minutesDifference / 60;
}// 分钟转小时
function convertMinutesToHours(minutes) {if (typeof minutes !== 'number' || minutes < 0) {return 'Invalid input';}const hours = Math.floor(minutes / 60);const remainingMinutes = Math.floor(minutes % 60);return {hours,remainingMinutes,};
}// 生成长度为n的二维空数组
function generateEmptyArray(n) {return Array.from({ length: n }, () => []);
}// 时间减去n个小时后的时间
function subtractHours(dateTimeStr, hoursToSubtract) {// 辅助函数:在数字前补零,确保总是两位数function padZero(number) {return number.toString().padStart(2, '0');}// 解析日期时间字符串为 Date 对象const dateTimeParts = dateTimeStr.split(' ');const dateParts = dateTimeParts[0].split('-');const timeParts = dateTimeParts[1].split(':');const dateObj = new Date(Date.UTC(dateParts[0], dateParts[1] - 1, dateParts[2], timeParts[0], timeParts[1]));// 减去指定的小时数dateObj.setUTCHours(dateObj.getUTCHours() - hoursToSubtract);// 格式化修改后的 Date 对象为字符串const formattedDate = `${dateObj.getUTCFullYear()}-${padZero(dateObj.getUTCMonth() + 1)}-${padZero(dateObj.getUTCDate())} ${padZero(dateObj.getUTCHours())}:${padZero(dateObj.getUTCMinutes())}`;// 返回结果return formattedDate;
}// 获取数组最大值
function findMaxWakeNum(arr) {if (arr.length === 0) {return null; // 如果数组为空,返回null或其他你认为合适的默认值}const maxNumObject = arr.reduce((max, current) => ((current.nightWakeTimes > max.nightWakeTimes) ? current : max));return maxNumObject.nightWakeTimes;
}// 获取数组最大值,如:10:00
function findMaxLeaveBedDate(arr) {// '2024-01-01 10:00' -> 10const changeNum = (str) => {if (!str) return 0;return parseInt(str.slice(11, 13));};if (!arr) {return null; // 如果数组为空,返回null或其他你认为合适的默认值}if (arr.length === 0) {return null; // 如果数组为空,返回null或其他你认为合适的默认值}const maxNumObject = arr.reduce((max, current) => ((changeNum(current.outOfBedDateTime) > changeNum(max.outOfBedDateTime)) ? current : max));if (!maxNumObject.outOfBedDateTime) return '10:00';const time = maxNumObject.outOfBedDateTime.slice(11, 16);const timeArr = time.split(':');let str = '';let tenthPlaceNum = '';const num = parseInt(timeArr[0]) + 1;if (num >= 10) {tenthPlaceNum = num;} else {tenthPlaceNum = `0${num}`;}if (timeArr[1] !== '00') {str = `${tenthPlaceNum}:00`;} else {str = time;}return str;
}// 数组转换
// [
// [1, 2],
// [3, 4],
// [5, 6],
// [7, 8],
// ];
// [
// [1, 3, 5, 7],
// [2, 4, 6, 8],
// ];
function transposeArray(arr) {if (arr.length === 0) {return []; // 如果数组为空,返回空数组或其他你认为合适的默认值}const numRows = arr.length;const numCols = arr[0].length;// 初始化新的二维数组const transposedArray = Array.from({ length: numCols }, () => Array(numRows).fill(0));// 转置数组for (let i = 0; i < numRows; i++) {for (let j = 0; j < numCols; j++) {transposedArray[j][i] = arr[i][j];}}return transposedArray;
}// '2024-01-01' -> 周一
function convertToWeekday(dateString) {const daysOfWeek = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];// 将日期字符串转换为Date对象const date = new Date(dateString);// 获取星期几的索引(0表示星期天,1表示星期一,以此类推)const dayOfWeekIndex = date.getDay();// 获取对应的星期几字符串const weekdayString = daysOfWeek[dayOfWeekIndex];return weekdayString;
}// 夜间醒来次数的最大值
const handleSourceData = (sourceData, newTime) => {console.log('测试-sourceData', sourceData);// 我需要的数据const allDataObj = {xData: [],bedData: [],bedOpData: [],sleepData: [],sleepOpData: [],lineDataTemp: generateEmptyArray(sourceData.statisticsVoList.length),lineOpDataTemp: generateEmptyArray(sourceData.statisticsVoList.length),lineData: generateEmptyArray(sourceData.statisticsVoList.length),lineOpData: generateEmptyArray(sourceData.statisticsVoList.length),polylineData: [],};const maxNum = findMaxWakeNum(sourceData.statisticsVoList);sourceData.statisticsVoList.forEach((item, index) => {// eslint-disable-next-line no-param-reassignitem.date = item.date.slice(0, 10);allDataObj.xData.push(convertToWeekday(item.date));// 1、黄色卧床透明柱子的数据const bedOpData = calculateHours(item.outOfBedDateTime, `${item.date} ${newTime}`);// 2、紫色睡眠透明柱子的数据const sleepOpData = calculateHours(item.wakeDateTime, `${item.date} ${newTime}`);// 3、黄色卧床柱子的数据const bedData = calculateHours(item.inBedDateTime, item.outOfBedDateTime);// 4、紫色睡眠柱子的数据const sleepData = calculateHours(item.asleepDateTime, item.wakeDateTime);// 5、柱子上的线,有个公式// const lineOpData = sleepOpData + (sleepData/(item.nightWakeTimes))*1// const lineData = 0.2allDataObj.bedData.push(bedData);allDataObj.bedOpData.push(bedOpData);allDataObj.sleepData.push(sleepData);allDataObj.sleepOpData.push(sleepOpData);if (item.nightWakeTimes) {for (let i = 0; i < item.nightWakeTimes; i++) {// 第n个线的oPdataconst lineOpData = sleepOpData + (sleepData / (item.nightWakeTimes + 1)) * (i + 1);// 第n个线的dataconst lineData = item.nightWakeTotalMinutes / 60 / item.nightWakeTimes;allDataObj.lineDataTemp[index].push(lineData);allDataObj.lineOpDataTemp[index].push(lineOpData);}} else {for (let i = 0; i < maxNum; i++) {allDataObj.lineDataTemp[index].push(0);allDataObj.lineOpDataTemp[index].push(0);}}allDataObj.lineData = transposeArray(allDataObj.lineDataTemp);allDataObj.lineOpData = transposeArray(allDataObj.lineOpDataTemp);// 6、折线图allDataObj.polylineData.push(item.sleepEfficiency);});return allDataObj;
};// 半夜醒来
const getNightWakeOption = (data, opData) => {let options = [];options = [// 透明柱子(黑色柱子下面){name: 'Placeholder2',type: 'bar',stack: 'Total', // 为透明柱子指定不同的 stackstackStrategy: 'negative',itemStyle: {color: 'rgba(0,0,0,0)',},data: opData,yAxisIndex: 0, // 指定使用左侧 y 轴z: 0,},// 黑色柱子{name: '半夜醒来',type: 'bar',stack: 'Total',// stackStrategy: 'negative',itemStyle: {color: lineColor,},// 条的高度data,yAxisIndex: 0, // 指定使用左侧 y 轴z: 1,},];return options;
};// 半夜醒来(获取多条)
const getNightWakeOptionAll = (data, opData) => {let arr = [];data.forEach((item, index) => {const arrTemp = getNightWakeOption(data[index], opData[index]);arr = [...arr, ...arrTemp];});return arr;
};// 卧床时长
const getBedTimeOption = (data, opData) => {let options = [];options = [// 透明柱子(黄色柱子下面){name: 'Placeholder1',type: 'bar',stack: 'Total', // 为透明柱子指定不同的 stack// stackStrategy: 'negative',itemStyle: {color: 'rgba(0,0,0,0)',},data: opData,yAxisIndex: 0, // 指定使用左侧 y 轴z: 0,},// 黄色柱子{name: '卧床时长',type: 'bar',stack: 'Total',data,itemStyle: {color: bigBarColor,barBorderRadius: [8, 8, 8, 8],},yAxisIndex: 0, // 指定使用左侧 y 轴z: 1,tooltip: {show: true,textStyle: {color: bigBarColor,},},},];return options;
};// 睡眠时长
const getSleepTimeOption = (data, opData) => {let options = [];options = [// 透明柱子(紫色柱子下面){name: 'Placeholder2',type: 'bar',stack: 'Total', // 为透明柱子指定不同的 stackstackStrategy: 'negative',itemStyle: {color: 'rgba(0,0,0,0)',},data: opData,yAxisIndex: 0, // 指定使用左侧 y 轴z: 0,},// 紫色柱子{name: '睡眠时长',type: 'bar',stack: 'Total',// stackStrategy: 'negative',itemStyle: {color: smallBarColor,},data,yAxisIndex: 0, // 指定使用左侧 y 轴z: 1,tooltip: {show: true,textStyle: {color: smallBarColor,},},},];return options;
};// 睡眠效率
const getSleepEfficiencyOption = (data) => {let options = [];options = [{name: '睡眠效率',type: 'line',data,symbol: 'circle',symbolSize: 8,lineStyle: {color: polylineColor, // 指定折线的颜色width: 2, // 指定折线的宽度},itemStyle: {color: polylineColor,borderColor: '#fff', // 白色边框borderWidth: 2, // 边框宽度},// 显示点上的数字label: {show: true,position: 'top',formatter(params) {// 在这里对数字进行转换return `${params.value}%`;},// color: '#333',},yAxisIndex: 1, // 指定使用右侧 y 轴tooltip: {show: true,textStyle: {color: polylineColor,},},},];return options;
};// 获取最终的图表配置
const getChartOptions = (allDataObj, newTime, thisWeekData) => {const option = {xAxis: {type: 'category',data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],axisLabel: {textStyle: {color: '#999', // 设置文字颜色},},axisTick: {show: false, // 设置不显示刻度点},},yAxis: [{type: 'value', // 左侧 y 轴配置interval: 2, // 设置刻度的间隔axisLabel: {textStyle: {color: '#999', // 设置文字颜色},formatter(value) {// 在这里进行刻度值的转化,第一个值其实可以是任何一天的10点,因为在图表y轴逻辑上不关心是哪一天的10:00const data = subtractHours(`2024-02-02 ${newTime}`, value).slice(11);return data;// return value;},},// min: 0, // 设置最小值为0max: thisWeekData.sleepMinutesAverage ? null : 10, // 设置最大值为100name: '时间', // 设置 y 轴的名称// nameGap: 20, // 设置 y 轴名称的偏移量nameTextStyle: {align: 'right',},},{type: 'value', // 右侧 y 轴配置name: '效率', // 设置 y 轴的名称nameTextStyle: {align: 'left',},splitLine: {show: false, // 右侧 Y 轴不显示横线},min: 0, // 设置最小值为0max: 100, // 设置最大值为100interval: 20, // 设置刻度间隔为20axisTick: {show: false,inside: false,length: 5,},axisLine: {show: false, // 不显示轴线},axisLabel: {textStyle: {color: '#999', // 设置文字颜色},},},],grid: {show: false,left: 50,top: 50,},legend: {orient: 'horizontal', // 设置图例的方向为水平bottom: 10, // 设置图例距离底部的距离itemGap: 20, // 设置图例项之间的距离data: ['卧床时长', '睡眠时长', '睡眠效率'], // 图例的名称,与 series 中的 name 对应// itemWidth: 12, // 设置图例项的宽度为10// itemHeight: 12, // 设置图例项的高度为10,即正方形},tooltip: {show: false,formatter(params) {let result = '';const { seriesName, seriesType, data } = params;if (seriesType === 'line') {result = `${seriesName} ${data}%`;}if (seriesType === 'bar') {const { hours, remainingMinutes } = convertMinutesToHours(data * 60);// 在这里对 Tooltip 中的数字进行转换result = `${seriesName} ${hours}h${remainingMinutes}m`;// result = `<view style="color: ${color};">${seriesName} ${data}</view><br>`;}return result;},// textStyle: {// color: '#8c7dc2',// },},series: [// 卧床时长...getBedTimeOption(allDataObj.bedData, allDataObj.bedOpData),// 睡眠时长...getSleepTimeOption(allDataObj.sleepData, allDataObj.sleepOpData),// 半夜醒来...getNightWakeOptionAll(allDataObj.lineData, allDataObj.lineOpData),// // 睡眠效率折线图...getSleepEfficiencyOption(allDataObj.polylineData),],};return option;
};const getResultChartOptions = (thisWeekData) => {// // 获取我要的周数据// const thisWeekData = getThisWeekData(sourceData);// 坐标上的10:00最新时间const newTime = findMaxLeaveBedDate(thisWeekData.statisticsVoList);// 处理得到我要的数据const allDataObjTemp = handleSourceData(thisWeekData, newTime);console.log('测试-处理得到我要的数据', allDataObjTemp);// 获取图表的配置const chartOptions = getChartOptions(allDataObjTemp, newTime, thisWeekData);return chartOptions;
};// 原始数据
const sourceData = [{// 周开始时间startDateTime: '2024-01-29',// 周结束时间endDateTime: '2024-02-04',// 平均睡眠时长sleepMinutesAverage: 2323,// 平均入睡时长asleepMinutesAverage: 1000,// 平均卧床时长inBedMinutesAverage: 2000,// 平均睡眠效率sleepEfficiencyAverage: 20,statisticsVoList: [{// 卧床开始时间inBedDateTime: '2024-01-28 22:00',// 卧床结束时间outOfBedDateTime: '2024-01-29 12:30',// 睡眠开始时间asleepDateTime: '2024-01-28 23:00',// 睡眠结束时间wakeDateTime: '2024-01-29 09:00',// 夜间起来次数nightWakeTimes: 3,// 夜间起来总时长nightWakeTotalMinutes: 20,// 睡眠效率sleepEfficiency: 80,// 日期date: '2024-01-29',// 睡眠时长sleepMinutes: 368},{// 卧床开始时间inBedDateTime: '2024-01-29 21:00',// 卧床结束时间outOfBedDateTime: '2024-01-30 09:00',// 睡眠开始时间asleepDateTime: '2024-01-30 00:00',// 睡眠结束时间wakeDateTime: '2024-01-30 06:00',// 夜间起来次数nightWakeTimes: 2,// 夜间起来总时长nightWakeTotalMinutes: 20,// 睡眠效率sleepEfficiency: 60,// 日期date: '2024-01-30',// 睡眠时长sleepMinutes: 368},{// 卧床开始时间inBedDateTime: '2024-01-30 20:00',// 卧床结束时间outOfBedDateTime: '2024-01-31 08:00',// 睡眠开始时间asleepDateTime: '2024-01-31 00:00',// 睡眠结束时间wakeDateTime: '2024-01-31 06:00',// 夜间起来次数nightWakeTimes: 0,// 夜间起来总时长nightWakeTotalMinutes: 20,// 睡眠效率sleepEfficiency: 70,// 日期date: '2024-01-31',// 睡眠时长sleepMinutes: 368},{// 卧床开始时间inBedDateTime: '2024-01-31 20:00',// 卧床结束时间outOfBedDateTime: '2024-02-01 09:00',// 睡眠开始时间asleepDateTime: '2024-01-31 23:00',// 睡眠结束时间wakeDateTime: '2024-02-01 06:00',// 夜间起来次数nightWakeTimes: 2,// 夜间起来总时长nightWakeTotalMinutes: 20,// 睡眠效率sleepEfficiency: 90,// 日期date: '2024-02-01',// 睡眠时长sleepMinutes: 368},]}
]const chartOptions = getResultChartOptions(sourceData[0])
chartOptions && myChart.setOption(chartOptions)
console.log('测试-chartOptions', chartOptions)
</script>
</html>