|
@@ -2,7 +2,7 @@
|
|
|
<div class="boxplot-container">
|
|
<div class="boxplot-container">
|
|
|
<div class="chart-container">
|
|
<div class="chart-container">
|
|
|
<div class="header">
|
|
<div class="header">
|
|
|
- <div class="chart-title">大气重金属浓度统计箱线图</div>
|
|
|
|
|
|
|
+ <div class="chart-title">灌溉水重金属浓度统计箱线图</div>
|
|
|
<p>展示各重金属浓度的分布特征(最小值、四分位数、中位数、最大值)</p>
|
|
<p>展示各重金属浓度的分布特征(最小值、四分位数、中位数、最大值)</p>
|
|
|
<p class="sample-subtitle">样本来源:{{ totalPoints }}个数据</p>
|
|
<p class="sample-subtitle">样本来源:{{ totalPoints }}个数据</p>
|
|
|
</div>
|
|
</div>
|
|
@@ -33,114 +33,72 @@ import { ref, onMounted ,computed} from 'vue'
|
|
|
export default {
|
|
export default {
|
|
|
components: { VChart },
|
|
components: { VChart },
|
|
|
setup() {
|
|
setup() {
|
|
|
- // -------- 核心配置 --------
|
|
|
|
|
- const apiUrl = ref('/api/vector/export/all?table_name=Atmo_sample_data') // 使用相对路径
|
|
|
|
|
- const heavyMetals = [
|
|
|
|
|
- { key: 'Cr_particulate', name: '铬 (Cr)', color: '#FF9800' },
|
|
|
|
|
- { key: 'As_particulate', name: '砷 (As)', color: '#4CAF50' },
|
|
|
|
|
- { key: 'Cd_particulate', name: '镉 (Cd)', color: '#9C27B0' },
|
|
|
|
|
- { key: 'Hg_particulate', name: '汞 (Hg)', color: '#2196F3' },
|
|
|
|
|
- { key: 'Pb_particulate', name: '铅 (Pb)', color: '#F44336' },
|
|
|
|
|
- // { key: 'particle_weight', name:'大气污染物重量' }
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ // -------- 基本状态 --------
|
|
|
|
|
+ const apiUrl = ref('/api/vector/export/all?table_name=Atmo_sample_data')
|
|
|
|
|
+ const apiTimestamp = ref(null)
|
|
|
|
|
+ const statsData = ref({}); // 新增:存储接口返回的预统计数据
|
|
|
|
|
+ const chartOption = ref({})
|
|
|
|
|
+ const isLoading = ref(true)
|
|
|
|
|
+ const error = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+ // 样本数统计(从预统计数据中获取)
|
|
|
|
|
+ const totalPoints = computed(() => {
|
|
|
|
|
+ const firstMetalKey = heavyMetals[0]?.key;
|
|
|
|
|
+ return statsData.value[firstMetalKey]?.count || 0;
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- // -------- 状态 --------
|
|
|
|
|
- const sampleData = ref([]) // 存储 properties 数据
|
|
|
|
|
- const chartOption = ref({}) // ECharts 配置
|
|
|
|
|
- const isLoading = ref(true) // 加载状态
|
|
|
|
|
- const error = ref(null) // 错误信息
|
|
|
|
|
- const statsByIndex = ref([]) // 缓存统计结果(与 x 轴对齐)
|
|
|
|
|
- const totalPoints = computed(() => sampleData.value.length)
|
|
|
|
|
|
|
+ // 缓存每个品类的统计量(与 x 轴顺序一致)
|
|
|
|
|
+ const statsByIndex = ref([])
|
|
|
|
|
|
|
|
|
|
+ // -------- 配置:金属字段 --------
|
|
|
|
|
+ const heavyMetals = [
|
|
|
|
|
+ { key: 'cr_concentration', name: '铬 (Cr)', color: '#FF9800' },
|
|
|
|
|
+ { key: 'as_concentration', name: '砷 (As)', color: '#4CAF50' },
|
|
|
|
|
+ { key: 'cd_concentration', name: '镉 (Cd)', color: '#9C27B0' },
|
|
|
|
|
+ { key: 'hg_concentration', name: '汞 (Hg)', color: '#2196F3' },
|
|
|
|
|
+ { key: 'pb_concentration', name: '铅 (Pb)', color: '#F44336' }
|
|
|
|
|
+ ]
|
|
|
|
|
|
|
|
- // -------- 工具函数 --------
|
|
|
|
|
- /** 日志工具(带颜色区分) */
|
|
|
|
|
|
|
+ // -------- 日志 --------
|
|
|
const log = (message, metalName = '') => {
|
|
const log = (message, metalName = '') => {
|
|
|
- console.log(
|
|
|
|
|
- `%c[${metalName || '全局'}] %c${message}`,
|
|
|
|
|
- 'color:#4CAF50;font-weight:bold',
|
|
|
|
|
- 'color:#333'
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /** 计算百分位数(线性插值法) */
|
|
|
|
|
- const calculatePercentile = (sortedArray, percentile) => {
|
|
|
|
|
- const n = sortedArray.length
|
|
|
|
|
- if (n === 0) return null
|
|
|
|
|
- if (percentile <= 0) return sortedArray[0]
|
|
|
|
|
- if (percentile >= 100) return sortedArray[n - 1]
|
|
|
|
|
- const index = (n - 1) * (percentile / 100)
|
|
|
|
|
- const lowerIndex = Math.floor(index)
|
|
|
|
|
- const upperIndex = lowerIndex + 1
|
|
|
|
|
- const fraction = index - lowerIndex
|
|
|
|
|
- if (upperIndex >= n) return sortedArray[lowerIndex]
|
|
|
|
|
- return sortedArray[lowerIndex] + fraction * (sortedArray[upperIndex] - sortedArray[lowerIndex])
|
|
|
|
|
|
|
+ console.log(`%c[${metalName || '全局'}] %c${message}`,
|
|
|
|
|
+ 'color:#4CAF50;font-weight:bold', 'color:#333')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // -------- 数据统计 --------
|
|
|
|
|
- /** 计算每个重金属的箱线图统计量(min/q1/median/q3/max) */
|
|
|
|
|
- const calculateBoxplotStats = () => {
|
|
|
|
|
- const stats = []
|
|
|
|
|
- heavyMetals.forEach((metal) => {
|
|
|
|
|
- //log(`开始处理 ${metal.name}`, metal.name)
|
|
|
|
|
- // 1. 提取原始值
|
|
|
|
|
- const rawValues = sampleData.value.map(item => item[metal.key])
|
|
|
|
|
- //log(`原始值:[${rawValues.slice(0, 5)}${rawValues.length > 5 ? ', ...' : ''}]`, metal.name)
|
|
|
|
|
|
|
+ // -------- 构建箱线数据 --------
|
|
|
|
|
+ const buildBoxplotData = () => {
|
|
|
|
|
+ const xAxisData = heavyMetals.map(m => m.name);
|
|
|
|
|
|
|
|
- // 2. 过滤无效值(NaN、非数字)
|
|
|
|
|
- const values = rawValues
|
|
|
|
|
- .map((val, idx) => {
|
|
|
|
|
- const num = Number(val)
|
|
|
|
|
- if (isNaN(num)) {
|
|
|
|
|
- log(`⚠️ 第${idx+1}条数据无效: ${val}`, metal.name)
|
|
|
|
|
- return null
|
|
|
|
|
- }
|
|
|
|
|
- return num
|
|
|
|
|
- })
|
|
|
|
|
- .filter(v => v !== null)
|
|
|
|
|
- //log(`有效数据量:${values.length} 条`, metal.name)
|
|
|
|
|
|
|
+ // 缓存每个重金属的统计量(用于tooltip)
|
|
|
|
|
+ statsByIndex.value = heavyMetals.map(metal => {
|
|
|
|
|
+ const stat = statsData.value[metal.key] || {};
|
|
|
|
|
+ return {
|
|
|
|
|
+ key: metal.key,
|
|
|
|
|
+ name: metal.name,
|
|
|
|
|
+ min: stat.min,
|
|
|
|
|
+ q1: stat.q1,
|
|
|
|
|
+ median: stat.median,
|
|
|
|
|
+ q3: stat.q3,
|
|
|
|
|
+ max: stat.max,
|
|
|
|
|
+ color: metal.color
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- // 3. 无有效数据时,记录空统计
|
|
|
|
|
- if (values.length === 0) {
|
|
|
|
|
- stats.push({ ...metal, min: null, q1: null, median: null, q3: null, max: null })
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ // 构建ECharts箱线图数据
|
|
|
|
|
+ const data = statsByIndex.value.map(s => {
|
|
|
|
|
+ if (s.min === undefined || s.min === null) {
|
|
|
|
|
+ return [null, null, null, null, null];
|
|
|
}
|
|
}
|
|
|
|
|
+ return [s.min, s.q1, s.median, s.q3, s.max];
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- // 4. 排序并计算统计量
|
|
|
|
|
- const sorted = [...values].sort((a, b) => a - b)
|
|
|
|
|
- const min = sorted[0]
|
|
|
|
|
- const max = sorted[sorted.length - 1]
|
|
|
|
|
- const q1 = calculatePercentile(sorted, 25)
|
|
|
|
|
- const median = calculatePercentile(sorted, 50)
|
|
|
|
|
- const q3 = calculatePercentile(sorted, 75)
|
|
|
|
|
-
|
|
|
|
|
- //log(`统计结果:min=${min}, q1=${q1}, median=${median}, q3=${q3}, max=${max}`, metal.name)
|
|
|
|
|
- stats.push({ ...metal, min, q1, median, q3, max })
|
|
|
|
|
- })
|
|
|
|
|
- return stats
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /** 构建 ECharts 箱线图数据 */
|
|
|
|
|
- const buildBoxplotData = (stats) => {
|
|
|
|
|
- const xAxisData = heavyMetals.map(m => m.name)
|
|
|
|
|
- // 与 x 轴顺序对齐(确保 tooltip 能正确匹配)
|
|
|
|
|
- statsByIndex.value = heavyMetals.map(m =>
|
|
|
|
|
- stats.find(s => s.key === m.key) || { ...m, min: null, q1: null, median: null, q3: null, max: null }
|
|
|
|
|
- )
|
|
|
|
|
- // 生成箱线图数据:[min, q1, median, q3, max]
|
|
|
|
|
- const data = statsByIndex.value.map(s =>
|
|
|
|
|
- s.min === null ? [null, null, null, null, null] : [s.min, s.q1, s.median, s.q3, s.max]
|
|
|
|
|
- )
|
|
|
|
|
- return { xAxisData, data }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return { xAxisData, data };
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- // -------- 图表初始化 --------
|
|
|
|
|
|
|
+ // -------- 初始化图表 --------
|
|
|
const initChart = () => {
|
|
const initChart = () => {
|
|
|
- //log('开始初始化图表')
|
|
|
|
|
- const stats = calculateBoxplotStats()
|
|
|
|
|
- const { xAxisData, data } = buildBoxplotData(stats)
|
|
|
|
|
|
|
+ const { xAxisData, data } = buildBoxplotData(); // 修复:直接调用构建函数
|
|
|
|
|
|
|
|
- // ECharts 配置(重点检查 series 数据格式)
|
|
|
|
|
chartOption.value = {
|
|
chartOption.value = {
|
|
|
tooltip: {
|
|
tooltip: {
|
|
|
trigger: 'item',
|
|
trigger: 'item',
|
|
@@ -149,7 +107,6 @@ export default {
|
|
|
if (!s || s.min === null) {
|
|
if (!s || s.min === null) {
|
|
|
return `<div style="font-weight:bold;color:#f56c6c">${xAxisData[params.dataIndex]}</div><div>无有效数据</div>`
|
|
return `<div style="font-weight:bold;color:#f56c6c">${xAxisData[params.dataIndex]}</div><div>无有效数据</div>`
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
return `<div style="font-weight:bold">${xAxisData[params.dataIndex]}</div>
|
|
return `<div style="font-weight:bold">${xAxisData[params.dataIndex]}</div>
|
|
|
<div style="margin-top:8px">
|
|
<div style="margin-top:8px">
|
|
|
<div>最小值:<span style="color:#5a5;">${s.min.toFixed(4)}</span></div>
|
|
<div>最小值:<span style="color:#5a5;">${s.min.toFixed(4)}</span></div>
|
|
@@ -158,28 +115,29 @@ export default {
|
|
|
<div>上四分位:<span style="color:#d87a80;">${s.q3.toFixed(4)}</span></div>
|
|
<div>上四分位:<span style="color:#d87a80;">${s.q3.toFixed(4)}</span></div>
|
|
|
<div>最大值:<span style="color:#5a5;">${s.max.toFixed(4)}</span></div>
|
|
<div>最大值:<span style="color:#5a5;">${s.max.toFixed(4)}</span></div>
|
|
|
</div>`
|
|
</div>`
|
|
|
- }
|
|
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
type: 'category',
|
|
type: 'category',
|
|
|
data: xAxisData,
|
|
data: xAxisData,
|
|
|
name: '重金属类型',
|
|
name: '重金属类型',
|
|
|
nameLocation: 'middle',
|
|
nameLocation: 'middle',
|
|
|
- nameGap: 45,
|
|
|
|
|
- axisLabel: { color: '#555', rotate: 30, fontWeight: 'bold',fontSize:11 }
|
|
|
|
|
|
|
+ nameGap: 30,
|
|
|
|
|
+ axisLabel: { color: '#555', rotate: 0, fontWeight: 'bold' ,fontSize :11}
|
|
|
},
|
|
},
|
|
|
yAxis: {
|
|
yAxis: {
|
|
|
type: 'value',
|
|
type: 'value',
|
|
|
- name: 'mg/kg',
|
|
|
|
|
|
|
+ name: 'ug/L',
|
|
|
|
|
+ nameTextStyle: { fontSize: 12 },
|
|
|
nameLocation: 'end',
|
|
nameLocation: 'end',
|
|
|
- nameGap: 5,
|
|
|
|
|
- axisLabel: { color: '#555', fontWeight: 'bold' ,fontSize:11},
|
|
|
|
|
|
|
+ nameGap: 8,
|
|
|
|
|
+ axisLabel: { color: '#555', fontWeight: 'bold',fontSize:11 },
|
|
|
splitLine: { lineStyle: { color: '#f0f0f0' } }
|
|
splitLine: { lineStyle: { color: '#f0f0f0' } }
|
|
|
},
|
|
},
|
|
|
series: [{
|
|
series: [{
|
|
|
name: '重金属浓度分布',
|
|
name: '重金属浓度分布',
|
|
|
type: 'boxplot',
|
|
type: 'boxplot',
|
|
|
- data, // 必须是 [[min,q1,median,q3,max], ...] 格式
|
|
|
|
|
|
|
+ data,
|
|
|
itemStyle: {
|
|
itemStyle: {
|
|
|
color: (p) => (heavyMetals[p.dataIndex]?.color || '#1890ff'),
|
|
color: (p) => (heavyMetals[p.dataIndex]?.color || '#1890ff'),
|
|
|
borderWidth: 2
|
|
borderWidth: 2
|
|
@@ -188,67 +146,50 @@ export default {
|
|
|
itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.2)', borderWidth: 3 }
|
|
itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.2)', borderWidth: 3 }
|
|
|
}
|
|
}
|
|
|
}],
|
|
}],
|
|
|
- grid: { top: '8%', right: '5%', left: '12%', bottom: '20%' }
|
|
|
|
|
|
|
+ grid: { top: '10%', right: '3%', left: '6%', bottom: '10%' }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
isLoading.value = false
|
|
isLoading.value = false
|
|
|
- //log('图表初始化完成')
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// -------- 接口请求 --------
|
|
// -------- 接口请求 --------
|
|
|
- onMounted(async () => {
|
|
|
|
|
- try {
|
|
|
|
|
- //log('发起API请求...')
|
|
|
|
|
- const response = await api8000.get(apiUrl.value) // 使用 api8000 实例
|
|
|
|
|
- //console.log('接口原始响应:', response.data) // 调试必看!
|
|
|
|
|
-
|
|
|
|
|
- let data = response.data
|
|
|
|
|
-
|
|
|
|
|
- // ✅ 兼容接口返回字符串的情况(比如后端没设置 application/json)
|
|
|
|
|
- if (typeof data === 'string') {
|
|
|
|
|
|
|
+ onMounted(async () => {
|
|
|
try {
|
|
try {
|
|
|
- // 兼容 NaN(非标准 JSON 值)→ 替换为 null
|
|
|
|
|
- data = JSON.parse(data.replace(/\bNaN\b/g, 'null'))
|
|
|
|
|
- } catch (parseErr) {
|
|
|
|
|
- throw new Error('接口返回的是字符串,但 JSON 解析失败')
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // log('发起API请求...')
|
|
|
|
|
+ const response = await api8000.get(apiUrl.value) // 使用 api8000 实例
|
|
|
|
|
+ // console.log('接口原始响应:', response.data) // 调试必看!
|
|
|
|
|
|
|
|
- // 1. 分情况提取 features(严格校验结构)
|
|
|
|
|
- let features = []
|
|
|
|
|
- if (data?.type === 'FeatureCollection') {
|
|
|
|
|
- // 情况1:标准 GeoJSON FeatureCollection
|
|
|
|
|
- if (Array.isArray(data.features)) {
|
|
|
|
|
- features = data.features
|
|
|
|
|
- } else {
|
|
|
|
|
- throw new Error('FeatureCollection 中 features 不是数组')
|
|
|
|
|
- }
|
|
|
|
|
- } else if (Array.isArray(data)) {
|
|
|
|
|
- // 情况2:直接返回 features 数组
|
|
|
|
|
- features = data
|
|
|
|
|
- } else {
|
|
|
|
|
- // 情况3:其他非法结构
|
|
|
|
|
- throw new Error(`接口结构异常,响应:${JSON.stringify(data)}`)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 2. 提取 properties 数据(确保非空)
|
|
|
|
|
- sampleData.value = features.map(f => f.properties)
|
|
|
|
|
- if (sampleData.value.length === 0) {
|
|
|
|
|
- throw new Error('接口返回数据为空(properties 为空)')
|
|
|
|
|
- }
|
|
|
|
|
- //log(`成功提取 ${sampleData.value.length} 条数据`, '接口')
|
|
|
|
|
-
|
|
|
|
|
- // 3. 初始化图表
|
|
|
|
|
- initChart()
|
|
|
|
|
|
|
+ let data = response.data
|
|
|
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- error.value = err
|
|
|
|
|
- isLoading.value = false
|
|
|
|
|
- console.error('接口请求失败:', err)
|
|
|
|
|
- }
|
|
|
|
|
-})
|
|
|
|
|
|
|
+ // ✅ 兼容接口返回字符串的情况(比如后端没设置 application/json)
|
|
|
|
|
+ if (typeof data === 'string') {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 尝试解析字符串为JSON
|
|
|
|
|
+ data = JSON.parse(data);
|
|
|
|
|
+ } catch (parseError) {
|
|
|
|
|
+ console.warn('接口返回字符串但解析失败,尝试重新请求', parseError);
|
|
|
|
|
+ // 如果解析失败,重新请求
|
|
|
|
|
+ const retryResponse = await api8000.get(apiUrl.value);
|
|
|
|
|
+ data = retryResponse.data;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ // 赋值并更新UI
|
|
|
|
|
+ statsData.value = data;
|
|
|
|
|
+ apiTimestamp.value = new Date().toLocaleString();
|
|
|
|
|
+ initChart();
|
|
|
|
|
+ isLoading.value = false;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ error.value = err;
|
|
|
|
|
+ isLoading.value = false;
|
|
|
|
|
+ console.error('接口请求失败:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
|
|
+ apiUrl,
|
|
|
|
|
+ apiTimestamp,
|
|
|
chartOption,
|
|
chartOption,
|
|
|
isLoading,
|
|
isLoading,
|
|
|
error,
|
|
error,
|
|
@@ -267,13 +208,14 @@ export default {
|
|
|
}
|
|
}
|
|
|
.header {
|
|
.header {
|
|
|
text-align: left;
|
|
text-align: left;
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
-}
|
|
|
|
|
-.chart-title {
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- color: #2980b9;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+.header h2 {
|
|
|
|
|
+ font-size: 0.6rem;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }
|
|
|
.header p {
|
|
.header p {
|
|
|
font-size: 0.6rem;
|
|
font-size: 0.6rem;
|
|
|
color: #666;
|
|
color: #666;
|
|
@@ -281,37 +223,42 @@ export default {
|
|
|
}
|
|
}
|
|
|
.loading-state {
|
|
.loading-state {
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
- padding: 40px 0;
|
|
|
|
|
- color: #666;
|
|
|
|
|
|
|
+ padding: 40px 0;
|
|
|
|
|
+ color: #666;
|
|
|
}
|
|
}
|
|
|
.loading-state .spinner {
|
|
.loading-state .spinner {
|
|
|
- display: inline-block;
|
|
|
|
|
- width: 24px;
|
|
|
|
|
- height: 24px;
|
|
|
|
|
- margin-right: 8px;
|
|
|
|
|
- border: 3px solid #ccc;
|
|
|
|
|
- border-top-color: #1890ff;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
|
|
+ display: inline-block; width: 24px; height: 24px; margin-right: 8px;
|
|
|
|
|
+ border: 3px solid #ccc; border-top-color: #1890ff; border-radius: 50%;
|
|
|
animation: spin 1s linear infinite;
|
|
animation: spin 1s linear infinite;
|
|
|
}
|
|
}
|
|
|
-@keyframes spin {
|
|
|
|
|
- to { transform: rotate(360deg); }
|
|
|
|
|
-}
|
|
|
|
|
-.error-state {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- padding: 40px 0;
|
|
|
|
|
- color: #f56c6c;
|
|
|
|
|
-}
|
|
|
|
|
-.chart-wrapper {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 220px; /* 确保高度有效 */
|
|
|
|
|
-}
|
|
|
|
|
|
|
+@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
|
+.error-state { text-align: center; padding: 40px 0; color: #f56c6c; }
|
|
|
|
|
+.chart-wrapper { width: 100%; height: 220px; }
|
|
|
.chart-container {
|
|
.chart-container {
|
|
|
background: white;
|
|
background: white;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
|
|
- padding: 20px;
|
|
|
|
|
margin-bottom: 25px;
|
|
margin-bottom: 25px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chart-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #2980b9;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.title-group {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: left;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.sample-subtitle {
|
|
.sample-subtitle {
|
|
@@ -319,5 +266,4 @@ export default {
|
|
|
color: #888;
|
|
color: #888;
|
|
|
margin-top: 4px;
|
|
margin-top: 4px;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-</style>
|
|
|
|
|
|
|
+</style>
|