framework7-vue
Version:
Build full featured iOS & Android apps using Framework7 & Vue
359 lines • 12.8 kB
JavaScript
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, resolveDynamicComponent as _resolveDynamicComponent, createBlock as _createBlock, normalizeClass as _normalizeClass, toDisplayString as _toDisplayString, createCommentVNode as _createCommentVNode, normalizeStyle as _normalizeStyle, createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, withCtx as _withCtx, renderSlot as _renderSlot } from "vue";
const _hoisted_1 = ["width", "height", "viewBox"];
const _hoisted_2 = ["data-index", "x1", "x2", "y2"];
const _hoisted_3 = {
key: 0,
class: "area-chart-axis"
};
const _hoisted_4 = {
key: 0
};
const _hoisted_5 = {
key: 1,
class: "area-chart-legend"
};
function render(_ctx, _cache) {
return _openBlock(), _createElementBlock("div", {
ref: "elRef",
class: _normalizeClass(_ctx.classes)
}, [(_openBlock(), _createElementBlock("svg", {
ref: "svgElRef",
xmlns: "http://www.w3.org/2000/svg",
width: _ctx.width,
height: _ctx.height,
viewBox: `0 0 ${_ctx.width} ${_ctx.height}`,
preserveAspectRatio: "none"
}, [(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.chartData, (data, index) => {
return _openBlock(), _createBlock(_resolveDynamicComponent(_ctx.ChartTag), {
key: `${_ctx.ChartTag}-${index}`,
fill: _ctx.lineChart ? undefined : data.color,
stroke: _ctx.lineChart ? data.color : undefined,
"fill-rule": "evenodd",
points: _ctx.lineChart ? undefined : data.points,
d: _ctx.lineChart ? data.points : undefined
}, null, 8, ["fill", "stroke", "points", "d"]);
}), 128)), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.verticalLines, (line, index) => {
return _openBlock(), _createElementBlock("line", {
key: `line-${index}`,
"data-index": index,
fill: "#000",
x1: line,
y1: 0,
x2: line,
y2: _ctx.height,
class: _normalizeClass(_ctx.classNames({
'area-chart-current-line': _ctx.currentIndex === index
}))
}, null, 10, _hoisted_2);
}), 128))], 8, _hoisted_1)), _ctx.axis ? (_openBlock(), _createElementBlock("div", _hoisted_3, [(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.axisLabels, (label, index) => {
return _openBlock(), _createElementBlock("span", {
key: index
}, [_ctx.visibleLegends.includes(label) ? (_openBlock(), _createElementBlock("span", _hoisted_4, _toDisplayString(_ctx.formatAxisLabelMethod(label)), 1)) : _createCommentVNode("", true)]);
}), 128))])) : _createCommentVNode("", true), _ctx.legend ? (_openBlock(), _createElementBlock("div", _hoisted_5, [(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.datasets, (dataset, index) => {
return _openBlock(), _createBlock(_resolveDynamicComponent(_ctx.LegendItemTag), {
key: index,
class: _normalizeClass(_ctx.classNames('area-chart-legend-item', {
'area-chart-legend-item-hidden': _ctx.hiddenDatasets.includes(index),
'area-chart-legend-button': _ctx.toggleDatasets
})),
type: _ctx.toggleDatasets ? 'button' : undefined,
onClick: $event => _ctx.toggleDataset(index)
}, {
default: _withCtx(() => [_createElementVNode("span", {
style: _normalizeStyle({
backgroundColor: dataset.color
})
}, null, 4), _createTextVNode(" " + _toDisplayString(_ctx.formatLegendLabelMethod(dataset.label)), 1)]),
_: 2
}, 1032, ["class", "type", "onClick"]);
}), 128))])) : _createCommentVNode("", true), _renderSlot(_ctx.$slots, "default")], 2);
}
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { classNames } from '../shared/utils.js';
import { f7 } from '../shared/f7.js';
export default {
name: 'f7-area-chart',
render,
props: {
lineChart: Boolean,
datasets: {
type: Array,
default: () => []
},
axis: Boolean,
axisLabels: {
type: Array,
default: () => []
},
tooltip: Boolean,
legend: Boolean,
toggleDatasets: Boolean,
width: {
type: Number,
default: 640
},
height: {
type: Number,
default: 320
},
maxAxisLabels: {
type: Number,
default: 8
},
formatAxisLabel: Function,
formatLegendLabel: Function,
formatTooltip: Function,
formatTooltipAxisLabel: Function,
formatTooltipTotal: Function,
formatTooltipDataset: Function
},
emits: ['select'],
setup(props, {
emit
}) {
let f7Tooltip = null;
const currentIndex = ref(null);
const hiddenDatasets = ref([]);
const elRef = ref(null);
const svgElRef = ref(null);
const linesOffsets = ref(null);
const visibleLegends = computed(() => {
if (!props.maxAxisLabels || props.axisLabels.length <= props.maxAxisLabels) return props.axisLabels;
const skipStep = Math.ceil(props.axisLabels.length / props.maxAxisLabels);
const filtered = props.axisLabels.filter((label, index) => index % skipStep === 0);
return filtered;
});
const summValues = computed(() => {
const summ = [];
props.datasets.filter((dataset, index) => !hiddenDatasets.value.includes(index)).forEach(({
values
}) => {
values.forEach((value, valueIndex) => {
if (!summ[valueIndex]) summ[valueIndex] = 0;
summ[valueIndex] += value;
});
});
return summ;
});
const chartData = computed(() => {
const {
datasets,
lineChart,
width,
height
} = props;
const data = [];
if (!datasets.length) {
return data;
}
const lastValues = datasets[0].values.map(() => 0);
let maxValue = 0;
if (lineChart) {
datasets.forEach(({
values
}) => {
const datasetMaxValue = Math.max(...values);
if (datasetMaxValue > maxValue) maxValue = datasetMaxValue;
});
} else {
maxValue = Math.max(...summValues.value);
}
datasets.filter((dataset, index) => !hiddenDatasets.value.includes(index)).forEach(({
label,
values,
color
}) => {
const points = values.map((originalValue, valueIndex) => {
lastValues[valueIndex] += originalValue;
const value = lineChart ? originalValue : lastValues[valueIndex];
const x = valueIndex / (values.length - 1) * width;
const y = height - value / maxValue * height;
if (lineChart) {
return `${valueIndex === 0 ? 'M' : 'L'}${x},${y}`;
}
return `${x} ${y}`;
});
if (!lineChart) {
points.push(`${width} ${height} 0 ${height}`);
}
data.push({
label,
points: points.join(' '),
color
});
});
return data.reverse();
});
const verticalLines = computed(() => {
const lines = [];
if (!props.datasets.length) {
return lines;
}
const values = props.datasets[0].values;
values.forEach((value, valueIndex) => {
const x = valueIndex / (values.length - 1) * props.width;
lines.push(x);
});
return lines;
});
const toggleDataset = index => {
if (!props.toggleDatasets) return;
if (hiddenDatasets.value.includes(index)) {
hiddenDatasets.value.splice(hiddenDatasets.value.indexOf(index), 1);
} else {
hiddenDatasets.value.push(index);
}
hiddenDatasets.value = [...hiddenDatasets.value];
};
const formatAxisLabelMethod = label => {
if (props.formatAxisLabel) return props.formatAxisLabel(label);
return label;
};
const formatLegendLabelMethod = label => {
if (props.formatLegendLabel) return props.formatLegendLabel(label);
return label;
};
const calcLinesOffsets = () => {
const lines = svgElRef.value.querySelectorAll('line');
linesOffsets.value = [];
for (let i = 0; i < lines.length; i += 1) {
linesOffsets.value.push(lines[i].getBoundingClientRect().left);
}
};
const formatTooltip = () => {
const index = currentIndex.value;
if (index === null) return '';
let total = 0;
const currentValues = props.datasets.filter((dataset, i) => !hiddenDatasets.value.includes(i)).map(dataset => ({
color: dataset.color,
label: dataset.label,
value: dataset.values[index]
}));
currentValues.forEach(dataset => {
total += dataset.value;
});
if (props.formatTooltip) {
return props.formatTooltip({
index,
total,
datasets: currentValues
});
}
let labelText = props.formatTooltipAxisLabel ? props.formatTooltipAxisLabel(props.axisLabels[index]) : formatAxisLabelMethod(props.axisLabels[index]);
if (!labelText) labelText = '';
const totalText = props.formatTooltipTotal ? props.formatTooltipTotal(total) : total;
// prettier-ignore
const datasetsText = currentValues.length > 0 ? `
<ul class="area-chart-tooltip-list">
${currentValues.map(({
label,
color,
value
}) => {
const valueText = props.formatTooltipDataset ? props.formatTooltipDataset(label, value, color) : `${label}: ${value}`;
return `
<li><span style="background-color: ${color};"></span>${valueText}</li>
`;
}).join('')}
</ul>` : '';
// prettier-ignore
return `
<div class="area-chart-tooltip-label">${labelText}</div>
<div class="area-chart-tooltip-total">${totalText}</div>
${datasetsText}
`;
};
const setTooltip = () => {
const {
tooltip,
datasets
} = props;
const index = currentIndex.value;
if (!tooltip) return;
const hasVisibleDataSets = datasets.filter((dataset, i) => !hiddenDatasets.value.includes(i)).length > 0;
if (!hasVisibleDataSets) {
if (f7Tooltip && f7Tooltip.hide) f7Tooltip.hide();
return;
}
if (index !== null && !f7Tooltip) {
f7Tooltip = f7.tooltip.create({
trigger: 'manual',
containerEl: elRef.value,
targetEl: svgElRef.value.querySelector(`line[data-index="${index}"]`),
text: formatTooltip(),
cssClass: 'area-chart-tooltip'
});
if (f7Tooltip && f7Tooltip.show) {
f7Tooltip.show();
}
return;
}
if (!f7Tooltip || !f7Tooltip.hide || !f7Tooltip.show) {
return;
}
if (index !== null) {
f7Tooltip.setText(formatTooltip());
f7Tooltip.setTargetEl(svgElRef.value.querySelector(`line[data-index="${index}"]`));
f7Tooltip.show();
} else {
f7Tooltip.hide();
}
};
const onMouseEnter = () => {
calcLinesOffsets();
};
const onMouseMove = e => {
if (!linesOffsets.value) {
calcLinesOffsets();
}
let currentLeft = e.pageX;
if (typeof currentLeft === 'undefined') currentLeft = 0;
const distances = linesOffsets.value.map(left => Math.abs(currentLeft - left));
const minDistance = Math.min(...distances);
const closestIndex = distances.indexOf(minDistance);
currentIndex.value = closestIndex;
};
const onMouseLeave = () => {
currentIndex.value = null;
};
watch(() => currentIndex.value, () => {
emit('select', currentIndex.value);
setTooltip();
});
onMounted(() => {
if (!svgElRef.value) return;
svgElRef.value.addEventListener('mouseenter', onMouseEnter);
svgElRef.value.addEventListener('mousemove', onMouseMove);
svgElRef.value.addEventListener('mouseleave', onMouseLeave);
});
onBeforeUnmount(() => {
if (f7Tooltip && f7Tooltip.destroy) {
f7Tooltip.destroy();
}
f7Tooltip = null;
if (!svgElRef.value) return;
svgElRef.value.removeEventListener('mouseenter', onMouseEnter);
svgElRef.value.removeEventListener('mousemove', onMouseMove);
svgElRef.value.removeEventListener('mouseleave', onMouseLeave);
});
const classes = computed(() => classNames('area-chart'));
const LegendItemTag = computed(() => props.toggleDatasets ? 'button' : 'span');
const ChartTag = computed(() => props.lineChart ? 'path' : 'polygon');
return {
currentIndex,
hiddenDatasets,
visibleLegends,
chartData,
verticalLines,
elRef,
svgElRef,
classes,
toggleDataset,
formatAxisLabelMethod,
formatLegendLabelMethod,
LegendItemTag,
ChartTag,
classNames
};
}
};