提交 4cd48246 编写于 作者: N Nicky Chan 提交者: daminglu

Redesign VisualDL for v1.2 (#502)

上级 1c976b75
# RELEASE 1.2.0
## New design
- Cleaner and more organized interface
- Show / select runs in global bar instead of individual tab
- Merge Scalar and Histogram into Metrics
- Merge Image, Audio and Text into Samples
- New Config design, add filter types in config
- New tags bar design instead of expand panel, easier to filter by tags
- New search bar integrated on tags bar
- Improve performance by showing less duplicate charts with tabs bar design
# RELEASE 1.1.0 # RELEASE 1.1.0
## New Features ## New Features
......
...@@ -17,14 +17,14 @@ export default { ...@@ -17,14 +17,14 @@ export default {
}, },
data() { data() {
return { return {
initialRoute: 'scalars', initialRoute: 'metrics',
}; };
}, },
created() { created() {
if (location.hash && location.hash != '#/') { if (location.hash && location.hash != '#/') {
this.initialRoute = /(\#\/)(\w*)([?|&]{0,1})/.exec(location.hash)[2]; this.initialRoute = /(\#\/)(\w*)([?|&]{0,1})/.exec(location.hash)[2];
} else { } else {
location.hash = '#/scalars'; location.hash = '#/metrics';
} }
}, },
}; };
......
...@@ -72,29 +72,14 @@ export default { ...@@ -72,29 +72,14 @@ export default {
selected: this.initialRoute, selected: this.initialRoute,
items: [ items: [
{ {
url: '/scalars', url: '/metrics',
title: 'SCALARS', title: 'METRICS',
name: 'scalars', name: 'metrics',
}, },
{ {
url: '/histograms', url: '/samples',
title: 'HISTOGRAMS', title: 'SAMPLES',
name: 'histograms', name: 'samples',
},
{
url: '/images',
title: 'IMAGES',
name: 'images',
},
{
url: '/audio',
title: 'AUDIO',
name: 'audio',
},
{
url: '/texts',
title: 'TEXTS',
name: 'texts',
}, },
{ {
url: '/graphs', url: '/graphs',
......
<template>
<v-card
hover
color="tag_background"
class="visual-dl-tags-tab">
<div
@click="$emit('click')">
<span :class="active ? 'visual-dl-tags-tab-text-active':'visual-dl-tags-tab-text-inactive' ">{{ title }} &nbsp; ({{ total }})</span>
</div>
</v-card>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
total: {
type: Number,
required: true,
},
active: {
type: Boolean,
required: true,
},
},
};
</script>
<style lang="stylus">
.visual-dl-tags-search-input
outline none
font-size 12px
.visual-dl-tags-tab
border-radius 17px
line-height 34px
height 34px
padding 0 14px
margin-right 16px
cursor pointer
position relative
display inline-block
.visual-dl-tags-tab-text-active
font-size 12px
font-weight bold
.visual-dl-tags-tab-text-inactive
font-size 12px
font-weight normal
color #555555
</style>
...@@ -11,7 +11,8 @@ Vue.use(Vuetify, { ...@@ -11,7 +11,8 @@ Vue.use(Vuetify, {
primary: '#008c99', primary: '#008c99',
accent: '#008c99', accent: '#008c99',
toolbox_icon: '#999999', toolbox_icon: '#999999',
dark_primary: '#00727c' dark_primary: '#00727c',
tag_background: '#f5f5f5',
}, },
}); });
......
<template>
<div class="visual-dl-page-container">
<div class="visual-dl-page-left">
<div>
<v-card
hover
color="tag_background"
class="visual-dl-tags-tab">
<v-icon>search</v-icon>
<input type="search" v-model="config.groupNameReg"
autocomplete="false"
placeholder="Search tags in RegExp"
class="visual-dl-tags-search-input">
</v-card>
<ui-tags-tab
:total="tagsListCount(allTagsMatchingList)"
:title="config.groupNameReg.trim().length == 0 ? 'All' : config.groupNameReg"
:active="selectedGroup === '' "
@click="selectedGroup = '' "
/>
<ui-tags-tab
v-for="item in groupedTags"
:total="tagsListCount(item.tags)"
:title="item.group"
:active="item.group === selectedGroup"
@click="selectedGroup = item.group"
/>
</div>
<ui-chart-page
:config="config"
:tag-list="finalTagsList"
:total="tagsListCount(finalTagsList)"
/>
</div>
<div class="visual-dl-page-right">
<div class="visual-dl-page-config-container">
<ui-config
:config="config"
/>
</div>
</div>
</div>
</template>
<script>
import {getPluginScalarsTags, getPluginHistogramsTags} from '../service';
import {cloneDeep, flatten, uniq} from 'lodash';
import autoAdjustHeight from '../common/util/autoAdjustHeight';
import TagsTab from '../common/component/TagsTab';
import Config from './ui/Config';
import ChartPage from './ui/ChartPage';
export default {
components: {
'ui-config': Config,
'ui-chart-page': ChartPage,
'ui-tags-tab': TagsTab,
},
props: {
runs: {
type: Array,
required: true,
},
},
data() {
return {
tagInfo: {scalar: {}, histogram: {}},
config: {
groupNameReg: '',
// scalar 'enabled' will be false when no scalar logs available, 'display' is toggled by user in config
scalar: {enabled: false, display: false},
histogram: {enabled: false, display: false},
smoothing: 0.6,
horizontal: 'step',
sortingMethod: 'default',
outlier: false,
runs: [],
running: true,
chartType: 'offset',
},
filteredTagsList: {scalar: [], histogram: []},
selectedGroup: '',
};
},
computed: {
finalTagsList() {
if (this.selectedGroup === '') {
return this.allTagsMatchingList;
} else {
let list;
this.groupedTags.forEach((item) => {
if (item.group === this.selectedGroup) {
list = item.tags;
}
});
return list;
}
},
allTagsMatchingList() {
let list = cloneDeep(this.filteredTagsList);
list.scalar = this.filteredListByRunsForScalar(list.scalar);
list.histogram = this.filteredListByRunsForHistogram(list.histogram);
return list;
},
tagsList() {
let list = {};
Object.keys(this.tagInfo).forEach((type) => {
let tags = this.tagInfo[type];
let runs = Object.keys(tags);
let tagsArray = runs.map((run) => Object.keys(tags[run]));
let allUniqTags = uniq(flatten(tagsArray));
// get the data for every chart
let tagsForEachType = allUniqTags.map((tag) => {
let tagList = runs.map((run) => {
return {
run,
tag: tags[run][tag],
};
}).filter((item) => item.tag !== undefined);
return {
tagList,
tag,
group: tag.split('/')[0],
};
});
list[type] = tagsForEachType;
});
return list;
},
groupedTags() {
let tagsList = this.tagsList || [];
// put data in group
let groupData = {};
Object.keys(tagsList).forEach((type) => {
let tagsForEachType = tagsList[type];
tagsForEachType.forEach((item) => {
let group = item.group;
if (groupData[group] === undefined) {
groupData[group] = {};
}
if (groupData[group][type] === undefined) {
groupData[group][type] = [];
}
groupData[group][type].push(item);
});
});
// to array
let groups = Object.keys(groupData);
let groupList = groups.map((group) => {
groupData[group].scalar = this.filteredListByRunsForScalar(groupData[group].scalar);
groupData[group].histogram = this.filteredListByRunsForHistogram(groupData[group].histogram);
return {
group,
tags: groupData[group],
};
});
return groupList;
},
},
created() {
getPluginScalarsTags().then(({errno, data}) => {
if (!data) return;
this.tagInfo.scalar = data;
this.config.scalar.enabled = true;
this.config.scalar.display = true;
this.filterTagsList(this.config.groupNameReg);
});
getPluginHistogramsTags().then(({errno, data}) => {
if (!data) return;
this.tagInfo.histogram = data;
this.config.histogram.enabled = true;
this.config.histogram.display = true;
this.filterTagsList(this.config.groupNameReg);
});
this.config.runs = this.runs;
},
mounted() {
autoAdjustHeight();
},
watch: {
'config.groupNameReg': function(val) {
this.throttledFilterTagsList();
},
'runs': function(val) {
this.config.runs = val;
},
},
methods: {
filterTagsList(groupNameReg) {
if (!groupNameReg || groupNameReg.trim().length == 0) {
this.filteredTagsList = cloneDeep(this.tagsList);
return;
}
this.selectedGroup = '';
let tagsList = this.tagsList || {};
let regExp = new RegExp(groupNameReg);
Object.keys(tagsList).forEach((type) => {
let tagsForEachType = tagsList[type];
this.filteredTagsList[type] = tagsForEachType.filter((item) => regExp.test(item.tag));
});
},
throttledFilterTagsList: _.debounce(
function() {
this.filterTagsList(this.config.groupNameReg);
}, 300
),
filteredListByRunsForScalar(scalar) {
if (!this.config.scalar.display) return [];
let runs = this.config.runs || [];
let list = cloneDeep(scalar) || [];
list = list.map((item) => {
item.tagList = item.tagList.filter((one) => runs.includes(one.run));
return item;
});
return list.filter((item) => item.tagList.length > 0);
},
filteredListByRunsForHistogram(histogram) {
if (!this.config.histogram.display) return [];
let runs = this.config.runs || [];
let list = cloneDeep(histogram) || [];
return flatten(list.map((item) => {
return item.tagList.filter((one) => runs.includes(one.run));
}));
},
tagsListCount(tagsList) {
let count = 0;
if (tagsList.scalar !== undefined) count += tagsList.scalar.length;
if (tagsList.histogram !== undefined) count += tagsList.histogram.length;
return count;
},
},
};
</script>
<style lang="stylus">
</style>
import {min, max, range} from 'lodash';
export function tansformBackendData(histogramData) {
let [time, step, items] = histogramData;
return {
time,
step,
min: min(items.map(([left, right, count]) => left)),
max: max(items.map(([left, right, count]) => right)),
items: items.map(([left, right, count]) => ({left, right, count})),
};
}
export function computeNewHistogram(histogram, min, max, binsNum = 30) {
if (max === min) {
// Create bins even if all the data has a single value.
max = min * 1.1 + 1;
min = min / 1.1 - 1;
}
let stepWidth = (max - min) / binsNum;
let itemIndex = 0;
return range(min, max, stepWidth).map((binLeft) => {
let binRight = binLeft + stepWidth;
let yValue = 0;
while (itemIndex < histogram.items.length) {
let itemRight = Math.min(max, histogram.items[itemIndex].right);
let itemLeft = Math.max(min, histogram.items[itemIndex].left);
let overlap = Math.min(itemRight, binRight) - Math.max(itemLeft, binLeft);
let count = (overlap / (itemRight - itemLeft)) * histogram.items[itemIndex].count;
yValue += overlap > 0 ? count : 0;
// If `itemRight` is bigger than `binRight`, then this bin is
// finished and there also has data for the next bin, so don't increment
// `itemIndex`.
if (itemRight > binRight) {
break;
}
itemIndex++;
}
return {x: binLeft, dx: stepWidth, y: yValue};
});
}
export function tansformToVisData(tempData, time, step) {
return tempData.map(function(dataItem) {
return [time, step, dataItem.x + dataItem.dx / 2, Math.floor(dataItem.y)];
});
}
export function originDataToChartData(originData) {
let tempData = originData.map(tansformBackendData);
let globalMin = min(tempData.map(({min}) => min));
let globalMax = max(tempData.map(({max}) => max));
let chartData = tempData.map(function(item) {
let histoBins = computeNewHistogram(item, globalMin, globalMax);
let {time, step} = item;
return {
time,
step,
items: tansformToVisData(histoBins, time, step),
};
});
return {
min: globalMin,
max: globalMax,
chartData,
};
}
<template>
<div class="visual-dl-chart-page">
<div
ref="chartPageBox"
class="visual-dl-chart-page-box">
<ui-scalar-chart
v-for="(tagInfo, index) in filteredScalarTagList"
:tag-info="tagInfo"
:smoothing="config.smoothing"
:horizontal="config.horizontal"
:sorting-method="config.sortingMethod"
:outlier="config.outlier"
:runs="config.runs"
:running="config.running"
/>
<ui-histogram-chart
v-for="(tagInfo, index) in filteredHistogramTagList"
:tag-info="tagInfo"
:runs="config.runs"
:chart-type="config.chartType"
:running="config.running"
/>
</div>
<v-pagination
v-if="total > pageSize"
v-model="currentPage"
:length="pageLength"
/>
</div>
</template>
<script>
import ScalarChart from './ScalarChart';
import HistogramChart from './HistogramChart';
export default {
components: {
'ui-scalar-chart': ScalarChart,
'ui-histogram-chart': HistogramChart,
},
props: {
config: {
type: Object,
required: true,
},
tagList: {
type: Object,
required: true,
},
total: {
type: Number,
required: true,
},
},
data() {
return {
// current page
currentPage: 1,
// item per page
pageSize: 12,
};
},
computed: {
filteredScalarTagList() {
return this.tagList.scalar.slice((this.currentPage - 1) * this.pageSize, this.currentPage * this.pageSize);
},
filteredHistogramTagList() {
let offset = this.tagList.scalar.length;
let start = (this.currentPage - 1) * this.pageSize - offset;
if (start < 0) start = 0;
let end = this.currentPage * this.pageSize - offset;
if (end < 0) end = 0;
return this.tagList.histogram.slice(start, end);
},
pageLength() {
return Math.ceil(this.total / this.pageSize);
},
},
watch: {
'config.runs': function(val) {
this.currentPage = 1;
},
tagList: function(val) {
this.currentPage = 1;
},
},
};
</script>
<style lang="stylus">
@import '~style/variables';
+prefix-classes('visual-dl-')
.chart-page
.chart-page-box:after
content: "";
clear: both;
display: block;
padding-bottom: 2%
</style>
<template>
<div class="visual-dl-page-config-com">
<v-checkbox
class="visual-dl-page-config-checkbox"
label="Scalars"
v-model="config.scalar.display"
:disabled="!config.scalar.enabled"
dark/>
<div class="visual-dl-page-component-block">
<div class="visual-dl-page-control-block">
<span :class="'visual-dl-page-control-span' + (config.scalar.display ? '' : ' visual-dl-page-disabled-text')">Smoothing</span>
<v-slider
:max="0.99"
:min="0"
:step="0.01"
v-model="smoothingValue"
class="visual-dl-page-smoothing-slider"
dark
:disabled="!config.scalar.display"/>
<span :class="'visual-dl-page-slider-span' + (config.scalar.display ? '' : ' visual-dl-page-disabled-text')">{{ smoothingValue }}</span>
</div>
<div class="visual-dl-page-control-block">
<span :class="'visual-dl-page-control-span' + (config.scalar.display ? '' : ' visual-dl-page-disabled-text')">X-axis</span>
<v-select
:items="horizontalItems"
v-model="config.horizontal"
class="visual-dl-page-config-selector"
dark
dense
:disabled="!config.scalar.display"
/>
</div>
<div class="visual-dl-page-control-block">
<span :class="'visual-dl-page-control-span' + (config.scalar.display ? '' : ' visual-dl-page-disabled-text')">Tooltip sorting</span>
<v-select
:items="sortingMethodItems"
v-model="config.sortingMethod"
class="visual-dl-page-config-selector"
dark
dense
:disabled="!config.scalar.display"
/>
</div>
<v-checkbox
class="visual-dl-page-outliers-checkbox"
label="Ignore outliers in chart scaling"
v-model="config.outlier"
dark
:disabled="!config.scalar.display"/>
</div>
<v-checkbox
class="visual-dl-page-config-checkbox"
label="Histogram"
v-model="config.histogram.display"
:disabled="!config.histogram.enabled"
dark/>
<div class="visual-dl-page-component-block">
<div class="visual-dl-page-control-block">
<span :class="'visual-dl-page-control-span' + (config.histogram.display ? '' : ' visual-dl-page-disabled-text')">Mode</span>
<v-select
:items="chartTypeItems"
v-model="config.chartType"
class="visual-dl-page-config-selector"
dark
dense
:disabled="!config.histogram.display"
/>
</div>
</div>
<v-btn
:color="config.running ? 'primary' : 'error'"
v-model="config.running"
v-if="!isDemo"
@click="toggleAllRuns"
class="visual-dl-page-run-toggle"
dark
block
>
{{ config.running ? 'Running' : 'Stopped' }}
</v-btn>
</div>
</template>
<script>
export default {
props: {
config: {
type: Object,
required: true,
},
},
data() {
return {
horizontalItems: [
{
text: 'Step',
value: 'step',
},
{
text: 'Relative',
value: 'relative',
},
{
text: 'Wall Time',
value: 'wall',
},
],
sortingMethodItems: [
'default', 'descending', 'ascending', 'nearest',
],
chartTypeItems: [
{
text: 'Overlay',
value: 'overlay',
},
{
text: 'Offset',
value: 'offset',
},
],
smoothingValue: this.config.smoothing,
isDemo: process.env.NODE_ENV === 'demo',
};
},
watch: {
smoothingValue: _.debounce(
function() {
this.config.smoothing = this.smoothingValue;
}, 50
),
},
methods: {
toggleAllRuns() {
this.config.running = !this.config.running;
},
},
};
</script>
<style lang="stylus">
+prefix-classes('visual-dl-page-')
.config-com
padding 20px
.component-block
padding-left 33px
padding-bottom 20px
margin-top -10px
.control-block
height 36px
display flex
align-items center
.control-span
font-size 12px
width 110px
margin-top 8px
.disabled-text
opacity 0.5
.smoothing-slider
display inline
.slider-span
width 35px
font-size 13px
.run-toggle
margin-top 20px
.config-checkbox label
font-size 13px
font-weight bold
.outliers-checkbox
margin-top 10px
.outliers-checkbox label
font-size 12px
.input-group--select .input-group__selections__comma
font-size 12px
</style>
<template>
<v-card
hover
class="visual-dl-page-charts">
<div
class="visual-dl-chart-box"
ref="visual_dl_chart_box"/>
<div class="visual-dl-chart-actions">
<v-btn
color="toolbox_icon"
flat
icon
@click="isExpand = !isExpand"
class="chart-toolbox-icons" >
<img
v-if="!isExpand"
src="../../assets/ic_fullscreen_off.svg">
<img
v-if="isExpand"
src="../../assets/ic_fullscreen_on.svg">
</v-btn>
</div>
</v-card>
</template>
<script>
// libs
import echarts from 'echarts';
import {originDataToChartData} from '../histogramHelper';
import {format, precisionRound} from 'd3-format';
// service
import {getPluginHistogramsHistograms} from '../../service';
let zrDrawElement = {};
zrDrawElement.hoverDots = [];
// the time to refresh chart data
const intervalTime = 15;
const p = Math.max(0, precisionRound(0.01, 1.01) - 1);
const yValueFormat = format('.' + p + 'e');
export default {
props: {
tagInfo: {
type: Object,
required: true,
},
runs: {
type: Array,
required: true,
},
running: {
type: Boolean,
required: true,
},
chartType: {
type: String,
required: true,
},
},
data() {
return {
originData: [],
isExpand: false,
isDemo: process.env.NODE_ENV === 'demo',
};
},
watch: {
tagInfo: function(val) {
this.initChart(val);
this.stopInterval();
if (this.running && !this.isDemo) {
this.startInterval();
}
},
originData: function(val) {
this.initChartOption();
},
chartType: function(val) {
this.initChartOption();
},
running: function(val) {
(val && !this.isDemo) ? this.startInterval() : this.stopInterval();
},
isExpand: function(val) {
this.expandArea(val);
},
},
mounted() {
let tagInfo = this.tagInfo;
this.initChart(tagInfo);
if (this.running && !this.isDemo) {
this.startInterval();
}
},
beforeDestroy() {
this.stopInterval();
},
methods: {
initChart(tagInfo) {
this.createChart();
this.getOriginChartData(tagInfo);
},
createChart() {
let el = this.$refs.visual_dl_chart_box;
this.myChart = echarts.init(el);
},
initChartOption() {
this.myChart.clear();
let zr = this.myChart.getZr();
let hoverDots = zrDrawElement.hoverDots;
if (hoverDots != null && hoverDots.length !== 0) {
hoverDots.forEach((dot) => zr.remove(dot));
}
let chartType = this.chartType;
let data = this.originData;
let visData = originDataToChartData(data);
let tagInfo = this.tagInfo;
let title = tagInfo.tag.displayName + '(' + tagInfo.run + ')';
this.setChartOptions(visData, title, chartType);
},
setChartOptions(visData, tag, chartType) {
let grid = {
left: 45,
top: 60,
right: 40,
bottom: 36,
};
let title = {
text: tag,
textStyle: {
fontSize: '12',
fontWeight: 'normal',
},
};
if (chartType === 'overlay') {
this.setOverlayChartOption(visData, title, grid);
} else if (chartType === 'offset') {
this.setOffsetChartOption(visData, title, grid);
}
},
setOverlayChartOption({chartData, min, max}, title, grid) {
let seriesOption = chartData.map(({time, step, items}) => ({
name: 'step' + step,
type: 'line',
showSymbol: false,
hoverAnimation: false,
z: 0,
data: items,
animationDuration: 100,
lineStyle: {
normal: {
width: 1,
color: '#008c99',
},
},
encode: {
x: [2],
y: [3],
},
})
);
let option = {
title: title,
axisPointer: {
link: {xAxisIndex: 'all'},
show: true,
snap: true,
triggerTooltip: true,
},
grid: grid,
xAxis: {
type: 'value',
},
yAxis: {
type: 'value',
axisLine: {
onZero: false,
},
axisLabel: {
formatter(value, index) {
return yValueFormat(value);
},
},
axisPointer: {
label: {
formatter({value}) {
return yValueFormat(value);
},
},
},
},
series: seriesOption,
};
let zr1 = this.myChart.getZr();
zr1.on('mousemove', function(e) {
zr1.remove(zrDrawElement.hoverLine);
zr1.remove(zrDrawElement.tooltip);
zr1.remove(zrDrawElement.tooltipX);
zr1.remove(zrDrawElement.tooltipY);
zrDrawElement.hoverDots.forEach((dot) => zr1.remove(dot));
zrDrawElement.hoverDots.length = 0;
});
this.myChart.setOption(option, {notMerge: true});
},
setOffsetChartOption({chartData, min, max}, title, grid) {
let rawData = [];
let minX = min;
let maxX = max;
let minZ = Infinity;
let maxZ = -Infinity;
let ecChart = this.myChart;
let maxStep = -Infinity;
let minStep = Infinity;
grid.top = 126;
grid.left = 16;
grid.right = 40;
chartData.forEach(function(dataItem) {
let lineData = [];
maxStep = Math.max(dataItem.step, maxStep);
minStep = Math.min(dataItem.step, minStep);
dataItem.items.forEach(([time, step, x, y]) => {
minZ = Math.min(minZ, y);
maxZ = Math.max(maxZ, y);
lineData.push(x, step, y);
});
rawData.push(lineData);
});
let option = {
textStyle: {
fontFamily: 'Merriweather Sans',
},
title,
color: ['#006069'],
visualMap: {
type: 'continuous',
show: false,
min: minStep,
max: maxStep,
dimension: 1,
inRange: {
colorLightness: [0.2, 0.4],
},
},
xAxis: {
min: minX,
max: maxX,
axisLine: {
onZero: false,
},
axisLabel: {
fontSize: '11',
formatter: function(value) {
return Math.round(value * 100) / 100;
},
},
splitLine: {
show: false,
},
},
yAxis: {
position: 'right',
axisLine: {
onZero: false,
},
inverse: true,
splitLine: {
show: false,
},
axisLabel: {
fontSize: '11',
},
},
grid,
series: [{
type: 'custom',
dimensions: ['x', 'y'],
renderItem: function(params, api) {
let points = makePolyPoints(
params.dataIndex,
api.value,
api.coord,
params.coordSys.y - 10
);
return {
type: 'polygon',
silent: true,
shape: {
points,
},
style: api.style({
stroke: '#bbb',
lineWidth: 1,
}),
};
},
data: rawData,
}],
};
function makePolyPoints(dataIndex, getValue, getCoord, yValueMapHeight) {
let points = [];
for (let i = 0; i < rawData[dataIndex].length;) {
let x = getValue(i++);
let y = getValue(i++);
let z = getValue(i++);
points.push(getPoint(x, y, z, getCoord, yValueMapHeight));
}
return points;
}
function getPoint(x, y, z, getCoord, yValueMapHeight) {
let pt = getCoord([x, y]);
// linear map in z axis
pt[1] -= (z - minZ) / (maxZ - minZ) * yValueMapHeight;
return pt;
}
let zr = ecChart.getZr();
function removeTooltip() {
if (zrDrawElement.hoverLine) {
zr.remove(zrDrawElement.hoverLine);
zr.remove(zrDrawElement.tooltip);
zrDrawElement.hoverDots.forEach((dot) => zr.remove(dot));
zrDrawElement.hoverDots.length = 0;
zr.remove(zrDrawElement.tooltipX);
zr.remove(zrDrawElement.tooltipY);
}
}
zr.on('mouseout', (e) => {
removeTooltip();
});
zr.on('mousemove', (e) => {
removeTooltip();
let nearestIndex = findNearestValue(e.offsetX, e.offsetY);
if (nearestIndex) {
let getCoord = function(pt) {
return ecChart.convertToPixel('grid', pt);
};
let gridRect = ecChart.getModel().getComponent('grid', 0).coordinateSystem.getRect();
let linePoints = makePolyPoints(
nearestIndex.itemIndex,
function(i) {
return rawData[nearestIndex.itemIndex][i];
},
getCoord,
gridRect.y - 10
);
zr.add(zrDrawElement.hoverLine = new echarts.graphic.Polyline({
silent: true,
shape: {
points: linePoints,
},
style: {
stroke: '#5c5c5c',
lineWidth: 2,
},
z: 999,
}));
let itemX;
rawData.forEach((dataItem) => {
let binIndex = nearestIndex.binIndex;
let x = dataItem[binIndex * 3];
let y = dataItem[binIndex * 3 + 1];
let z = dataItem[binIndex * 3 + 2];
let pt = getPoint(x, y, z, getCoord, gridRect.y - 10);
itemX = pt[0];
let dot = new echarts.graphic.Circle({
shape: {
cx: pt[0],
cy: pt[1],
r: 3,
},
style: {
fill: '#000',
stroke: '#ccc',
lineWidth: 1,
},
z: 1000,
});
zr.add(dot);
zrDrawElement.hoverDots.push(dot);
});
let hoveredItem = chartData[nearestIndex.itemIndex];
zrDrawElement.tooltip = new echarts.graphic.Text({
position: [e.offsetX + 30, e.offsetY - 50],
style: {
fontFamily: 'Merriweather Sans',
text: yValueFormat(hoveredItem.items[nearestIndex.binIndex][3]),
textFill: '#000',
fontSize: 14,
textBackgroundColor: '#eee',
textBorderColor: '#008c99',
textBorderWidth: 2,
textBorderRadius: 5,
textPadding: 10,
rich: {},
},
z: 2000,
});
zr.add(zrDrawElement.tooltip);
zrDrawElement.tooltipX = new echarts.graphic.Text({
position: [
itemX,
gridRect.y + gridRect.height,
],
style: {
fontFamily: 'Merriweather Sans',
text: Math.round(hoveredItem.items[nearestIndex.binIndex][2] * 1000) / 1000,
textFill: '#fff',
textAlign: 'center',
fontSize: 12,
textBackgroundColor: '#333',
textBorderWidth: 2,
textPadding: [5, 7],
rich: {},
},
z: 2000,
});
zr.add(zrDrawElement.tooltipX);
zrDrawElement.tooltipY = new echarts.graphic.Text({
position: [
gridRect.x + gridRect.width,
linePoints[linePoints.length - 1][1],
],
style: {
fontFamily: 'Merriweather Sans',
text: hoveredItem.step,
textFill: '#fff',
textVerticalAlign: 'middle',
fontSize: 12,
textBackgroundColor: '#333',
textBorderWidth: 2,
textPadding: [5, 7],
rich: {},
},
z: 2000,
});
zr.add(zrDrawElement.tooltipY);
}
});
function findNearestValue(px, py) {
let value = ecChart.convertFromPixel('grid', [px, py]);
let itemIndex;
let nearestY = Infinity;
let binIndex;
chartData.forEach((item, index) => {
let dist = Math.abs(value[1] - item.step);
if (dist < nearestY) {
nearestY = dist;
itemIndex = index;
}
});
if (itemIndex != null) {
let dataItem = chartData[itemIndex];
let nearestX = Infinity;
dataItem.items.forEach((item, index) => {
let dist = Math.abs(item[2] - value[0]);
if (dist < nearestX) {
nearestX = dist;
binIndex = index;
}
});
if (binIndex != null) {
return {
itemIndex: itemIndex,
binIndex: binIndex,
};
}
}
}
ecChart.setOption(option, {notMerge: true});
},
// get origin data per 60 seconds
startInterval() {
this.getOringDataInterval = setInterval(() => {
let tagInfo = this.tagInfo;
this.getOriginChartData(tagInfo);
}, intervalTime * 1000);
},
stopInterval() {
clearInterval(this.getOringDataInterval);
},
getOriginChartData(tagInfo) {
let run = tagInfo.run;
let tag = tagInfo.tag;
let params = {
run,
tag: tag.displayName,
};
getPluginHistogramsHistograms(params).then(({status, data}) => {
if (status === 0) {
this.originData = data;
}
});
},
expandArea(expand) {
let pageBoxWidth = document.getElementsByClassName('visual-dl-chart-page')[0].offsetWidth;
let width = pageBoxWidth * 0.96; // 4% margin
if (expand) {
let el = this.$refs.visual_dl_chart_box;
el.style.width = width + 'px';
el.style.height = '600px';
this.isExpand = true;
this.myChart.resize({
width: width,
height: 600,
});
} else {
let el = this.$refs.visual_dl_chart_box;
el.style.width = '400px';
el.style.height = '300px';
this.isExpand = false;
this.myChart.resize({
width: 400,
height: 300,
});
}
},
},
};
</script>
<style lang="stylus">
.visual-dl-page-charts
float left
margin 2% 2% 0 0
background #fff
padding 10px
position relative
.visual-dl-chart-box
width 400px
height 300px
.visual-dl-chart-actions
opacity 0
transition: opacity .3s ease-out;
position absolute
top 4px
right 10px
img
width 30px
height 30px
position absolute
top 0
bottom 0
margin auto
.chart-toolbox-icons
width 25px
height 25px
margin-left -4px
margin-right -4px
.visual-dl-page-charts:hover
.visual-dl-chart-actions
opacity 1
</style>
<template>
<v-card
hover
class="visual-dl-page-charts">
<div
ref="chartBox"
class="visual-dl-chart-box"
:style="computedStyle"/>
<div class="visual-dl-chart-actions">
<v-btn
color="toolbox_icon"
flat
icon
@click="isSelectZoomEnable = !isSelectZoomEnable"
class="chart-toolbox-icons">
<img
v-if="!isSelectZoomEnable"
src="../../assets/ic_zoom_select_off.svg">
<img
v-if="isSelectZoomEnable"
src="../../assets/ic_zoom_select_on.svg">
</v-btn>
<v-btn
color="toolbox_icon"
flat
icon
@click="restoreChart"
class="chart-toolbox-icons">
<img src="../../assets/ic_undo.svg">
</v-btn>
<v-btn
color="toolbox_icon"
flat
icon
@click="isExpand = !isExpand"
class="chart-toolbox-icons" >
<img
v-if="!isExpand"
src="../../assets/ic_fullscreen_off.svg">
<img
v-if="isExpand"
src="../../assets/ic_fullscreen_on.svg">
</v-btn>
<v-btn
color="toolbox_icon"
flat
icon
@click="saveChartAsImage"
class="chart-toolbox-icons" >
<img src="../../assets/ic_download.svg">
</v-btn>
<v-menu v-if="tagInfo.tagList.length > 0">
<v-btn
color="toolbox_icon"
slot="activator"
flat
icon
class="chart-toolbox-icons">
<v-icon >more_vert</v-icon>
</v-btn>
<v-list dense>
<v-list-tile>
<v-list-tile-content>
<v-list-tile-title>Download data in JSON</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-icon>expand_more</v-icon>
</v-list-tile-action>
</v-list-tile>
<v-list-tile
v-for="subItem in tagInfo.tagList"
:key="subItem.run"
@click="handleDownLoad(subItem.run)">
<v-list-tile-content>
<v-list-tile-title>&nbsp;&nbsp;&nbsp;{{ subItem.run }}</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-menu>
</div>
</v-card>
</template>
<script>
// libs
import echarts from 'echarts';
import axios from 'axios';
import {isFinite, flatten, maxBy, minBy, sortBy, max, min} from 'lodash';
import {generateJsonAndDownload} from '../../common/util/downLoadFile';
import {quantile} from '../../common/util/index';
import moment from 'moment';
// service
import {getPluginScalarsScalars} from '../../service';
const originLinesOpacity = 0.3;
const lineWidth = 1.5;
const minQuantile = 0.05;
const maxQuantile = 0.95;
// the time to refresh chart data
const intervalTime = 15;
export default {
props: {
tagInfo: {
type: Object,
required: true,
},
runs: {
type: Array,
required: true,
},
running: {
type: Boolean,
required: true,
},
smoothing: {
type: Number,
required: true,
},
horizontal: {
type: String,
required: true,
},
sortingMethod: {
type: String,
required: true,
},
outlier: {
type: Boolean,
required: true,
},
},
computed: {
computedStyle() {
return 'height:' + this.height + 'px;'
+ 'width:' + this.width + 'px;';
},
},
data() {
return {
isDemo: process.env.NODE_ENV === 'demo',
width: 400,
height: 300,
isExpand: false,
isSelectZoomEnable: true,
originData: [],
};
},
watch: {
originData: function(val) {
this.setChartData();
this.setChartsOutlier();
this.setChartHorizon();
},
smoothing: function(val) {
this.setChartData();
},
outlier: function(val) {
this.setChartsOutlier();
},
horizontal: function(val) {
this.setChartHorizon();
},
tagInfo: function(val) {
// Should Clean up the chart before each use.
this.myChart.clear();
this.setChartsOptions(val);
this.getOriginChartData(val);
},
isExpand: function(val) {
this.expandArea(val);
},
isSelectZoomEnable: function(val) {
this.toggleSelectZoom(val);
},
},
mounted() {
this.initChart(this.tagInfo);
this.toggleSelectZoom(true);
if (this.running && !this.isDemo) {
this.startInterval();
}
this.$watch('running', function(running) {
// if it is demo, do not trigger interval
running = running && !this.isDemo;
running ? this.startInterval() : this.stopInterval();
});
},
beforeDestroy() {
this.stopInterval();
},
methods: {
// Create a Scalar Chart, initialize it with default settings, then load datas
initChart(tagInfo) {
this.createChart();
this.setChartsOptions(tagInfo);
this.getOriginChartData(tagInfo);
},
createChart() {
let el = this.$refs.chartBox;
this.myChart = echarts.init(el);
},
setChartsOptions({tagList, tag}) {
// Create two lines, one line is original, the other line is for smoothing
let seriesOption = tagList.map((item) => [
{
name: item.run,
type: 'line',
showSymbol: false,
hoverAnimation: false,
z: 0,
data: [],
animationDuration: 100,
lineStyle: {
normal: {
opacity: originLinesOpacity,
width: lineWidth,
},
},
},
{
name: item.run,
type: 'line',
showSymbol: false,
hoverAnimation: false,
z: 1,
data: [],
animationDuration: 100,
lineStyle: {
normal: {
width: lineWidth,
},
},
},
]
);
seriesOption = flatten(seriesOption);
let legendOptions = tagList.map((item) => item.run);
let instance = this;
let option = {
textStyle: {
fontFamily: 'Merriweather Sans',
},
color: [
'#008c99',
'#c23531',
'#FF9900',
'#109618',
'#990099',
'#3B3EAC',
'#DD4477',
'#AAAA11',
'#5574A6',
'#8B0707',
],
title: {
text: tag,
textStyle: {
fontSize: 13,
fontWeight: 'normal',
},
},
tooltip: {
trigger: 'axis',
axisPointer: {
animation: true,
},
textStyle: {
fontSize: '13',
},
position: ['10%', '90%'],
formatter(params, ticket, callback) {
let data = instance.getFormatterPoints(params[0].data);
return instance.transformFormatterData(data);
},
},
toolbox: {
show: true,
showTitle: false,
itemSize: 0,
feature: {
dataZoom: {},
},
},
legend: {
data: legendOptions,
top: 39,
},
grid: {
left: 48,
top: 75,
right: 40,
bottom: 36,
},
xAxis: {
type: 'value',
name: this.horizontal,
axisLabel: {
fontSize: '11',
},
splitNumber: this.isExpand ? 10 : 5,
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: '11',
formatter(value) {
return value.toString().slice(0, 5);
},
},
},
series: seriesOption,
};
this.myChart.setOption(option);
},
// Get origin data per 60 seconds
startInterval() {
this.getOriginDataInterval = setInterval(() => {
this.getOriginChartData(this.tagInfo);
}, intervalTime * 1000);
},
stopInterval() {
clearInterval(this.getOriginDataInterval);
},
getOriginChartData({tagList, tag}) {
let requestList = tagList.map((item) => {
let params = {
run: item.run,
tag: tag,
};
return getPluginScalarsScalars(params);
});
axios.all(requestList).then((resArray) => {
if (resArray.every((res) => res.status === 0)) {
this.originData = resArray.map((res) => res.data);
}
});
},
setChartData() {
let seriesData = this.originData.map((lineData) => {
if (lineData.length == 0) return [];
// add the smoothed data
this.transformDataset(lineData);
return [
{
data: lineData,
encode: {
// map 1 dimension to xAixs.
x: [1],
// map 2 dimension to yAixs.
y: [2],
},
},
{
data: lineData,
encode: {
// Map 1 dimension to xAixs.
x: [1],
// Map 3 dimension to yAixs,
// the third number is smoothed value.
y: [3],
},
},
];
});
this.myChart.setOption({
series: flatten(seriesData),
});
},
getChartOptions() {
return this.myChart.getOption() || {};
},
handleDownLoad(runItemForDownload) {
let options = this.getChartOptions();
let series = options.series || [];
let seriesItem = series.find((item) => item.name === runItemForDownload) || {};
let fileName = this.tagInfo.tag.replace(/\//g, '-');
generateJsonAndDownload(seriesItem.data, fileName);
},
transformDataset(seriesData) {
// smooth
this.transformData(seriesData, this.smoothing);
},
/**
* @desc 1、add smooth data depend on smoothingWeight. see https://en.wikipedia.org/wiki/Moving_average for detail
* 2、add relative data
* @param {Object} seriesData: echarts series Object
* @param {number} smoothingWeight smooth weight, between 0 ~ 1
*/
transformData(seriesData, smoothingWeight) {
let data = seriesData;
let last = data.length > 0 ? 0 : NaN;
let numAccum = 0;
let startValue;
data.forEach((d, i) => {
let nextVal = d[2];
// second to millisecond.
let millisecond = Math.floor(d[0] * 1000);
if (i === 0) {
startValue = millisecond;
}
// Relative time, millisecond to hours.
d[4] = Math.floor(millisecond - startValue) / (60 * 60 * 1000);
if (!isFinite(nextVal)) {
d[3] = nextVal;
} else {
last = last * smoothingWeight + (1 - smoothingWeight) * nextVal;
numAccum++;
let debiasWeight = 1;
if (smoothingWeight !== 1.0) {
debiasWeight = 1.0 - Math.pow(smoothingWeight, numAccum);
}
d[3] = last / debiasWeight;
}
});
},
// Chart outlier options methods and functions ---- start.
// Compute Y domain from originData.
setChartsOutlier() {
let domainRangeArray = this.originData.map((seriesData) => this.computeDataRange(seriesData, this.outlier));
// Compare, get the best Y domain.
let flattenNumbers = flatten(domainRangeArray);
let finalMax = max(flattenNumbers);
let finalMin = min(flattenNumbers);
// Add padding.
let PaddedYDomain = this.paddedYDomain(finalMin, finalMax);
this.setChartOutlierOptions(PaddedYDomain);
// Store Y domain, if originData is not change, Y domain keep same.
},
// Compute max and min from array, if outlier is true, return quantile range.
computeDataRange(arr, isQuantile) {
// Get data range.
if (arr.length == 0) return [];
let max;
let min;
if (!isQuantile) {
// Get the orgin data range.
max = maxBy(arr, (item) => item[2])[2];
min = minBy(arr, (item) => item[2])[2];
} else {
// Get the quantile range.
let sorted = sortBy(arr, [(item) => item[2]]);
min = quantile(sorted, minQuantile, (item) => item[2]);
max = quantile(arr, maxQuantile, (item) => item[2]);
}
return [min, max];
},
paddedYDomain(min, max) {
return {
max: max > 0 ? max * 1.1 : max * 0.9,
min: min > 0 ? min * 0.9 : min * 1.1,
};
},
setChartOutlierOptions({min, max}) {
this.myChart.setOption({
yAxis: {
min,
max,
},
});
},
// Chart horizontal options methods and functions ---- start.
setChartHorizon() {
let seriesOption = this.myChart.getOption().series;
let encodeSeries = (val) => {
return {
encode: {
x: [val],
},
};
};
let stepSeries = seriesOption.map((item) => encodeSeries(1));
let relativeSeries = seriesOption.map((item) => encodeSeries(4));
let wallSeries = seriesOption.map((item) => encodeSeries(0));
let horizontalToxAxisOptions = {
step: {
xAxis: {
type: 'value',
name: this.horizontal,
axisLabel: {
fontSize: '11',
},
splitNumber: this.isExpand ? 10 : 5,
},
series: stepSeries,
},
relative: {
xAxis: {
type: 'value',
name: this.horizontal,
axisLabel: {
fontSize: '11',
},
splitNumber: this.isExpand ? 10 : 5,
},
series: relativeSeries,
},
wall: {
xAxis: {
type: 'time',
name: this.horizontal,
axisLabel: {
fontSize: '11',
formatter: function(value, index) {
// The value is in seconds, need to convert to milliseconds
let date = new Date(value * 1000);
return date.toLocaleTimeString();
},
},
},
series: wallSeries,
},
};
this.myChart.setOption(horizontalToxAxisOptions[this.horizontal]);
},
expandArea(expand) {
let pageBoxWidth = document.getElementsByClassName('visual-dl-chart-page-box')[0].offsetWidth;
let width = pageBoxWidth * 0.96; // 4% margin
if (expand) {
let el = this.$refs.chartBox;
el.style.width = width + 'px';
el.style.height = '600px';
this.myChart.resize({
width: width,
height: 600,
});
} else {
let el = this.$refs.chartBox;
el.style.width = '400px';
el.style.height = '300px';
this.myChart.resize({
width: 400,
height: 300,
});
}
this.myChart.setOption({
xAxis: {
splitNumber: this.isExpand ? 10 : 5,
},
});
},
toggleSelectZoom(enable) {
let instance = this;
setTimeout(function() {
instance.myChart.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: enable,
});
}, 0);
},
restoreChart() {
this.myChart.dispatchAction({
type: 'restore',
});
},
saveChartAsImage() {
let dataUrl = this.myChart.getDataURL({
pixelRatio: 1,
backgroundColor: '#fff',
});
let fileName = this.tagInfo.tag.replace(/\//g, '-');
let link = document.createElement('a');
link.download = fileName;
link.href = dataUrl;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
getFormatterPoints(data) {
let originData = this.originData;
let tagList = this.tagInfo.tagList;
let sortingMethod = this.sortingMethod;
// Can't know exactly the tigger runs.
// If the step is same, regard the point as the trigger point.
let [, step, triggerValue] = data;
let points = originData.map((series, index) => {
let nearestItem;
if (step === 0) {
nearestItem = series[0];
} else {
for (let i = 0; i < series.length; i++) {
let item = series[i];
if (item[1] === step) {
nearestItem = item;
break;
}
if (item[1] > step) {
let index = i - 1;
nearestItem = series[index >= 0 ? index : 0];
break;
}
if (!nearestItem) {
nearestItem = series[series.length - 1];
}
}
}
return {
run: tagList[index].run,
item: nearestItem,
};
});
if (sortingMethod === 'default' || !sortingMethod) {
return points;
}
let sortedPoints;
switch (sortingMethod) {
case 'descending':
sortedPoints = sortBy(points, (one) => one.item[3]);
sortedPoints.reverse();
break;
case 'ascending':
sortedPoints = sortBy(points, (one) => one.item[3]);
break;
case 'nearest':
// Compare other ponts width the trigger point, caculate the nearest sort.
sortedPoints = sortBy(points, (one) => one.item[3] - triggerValue);
break;
default:
sortedPoints = points;
}
return sortedPoints;
},
transformFormatterData(data) {
let indexPropMap = {
Time: 0,
Step: 1,
Value: 2,
Smoothed: 3,
Relative: 4,
};
let widthPropMap = {
Run: 60,
Time: 120,
Step: 40,
Value: 50,
Smoothed: 60,
Relative: 60,
};
let transformedData = data.map((item) => {
let data = item.item;
return {
Run: item.run,
// Keep six number for easy-read.
Smoothed: data[indexPropMap.Smoothed].toString().slice(0, 6),
Value: data[indexPropMap.Value].toString().slice(0, 6),
Step: data[indexPropMap.Step],
Time: moment(Math.floor(data[indexPropMap.Time] * 1000), 'x').format('YYYY-MM-DD HH:mm:ss'),
// Relative display value should take easy-read into consideration.
// Better to tranform data to 'day:hour', 'hour:minutes', 'minute: seconds' and second only.
Relative: Math.floor(data[indexPropMap.Relative] * 60 * 60) + 's',
};
});
let headerHtml = '<tr style="font-size:14px;">';
headerHtml += Object.keys(transformedData[0]).map((key) => {
return '<td style="padding: 0 4px; font-family: \'Merriweather Sans\'; font-weight: bold; width:' +
widthPropMap[key] + 'px;">' + key + '</td>';
}).join('');
headerHtml += '</tr>';
let content = transformedData.map((item) => {
let str = '<tr style="font-size:12px;">';
str += Object.keys(item).map((val) => {
return '<td style="padding: 0 4px; overflow: hidden;">' + item[val] + '</td>';
}).join('');
str += '</tr>';
return str;
}).join('');
return '<table style="text-align: left;table-layout: fixed;width: 500px;"><thead>' + headerHtml + '</thead>'
+ '<tbody>' + content + '</tbody><table>';
},
},
};
</script>
<style lang="stylus">
.visual-dl-page-charts
float left
margin 2% 2% 0 0
padding 10px
position relative
.visual-dl-chart-actions
opacity 0
transition: opacity .3s ease-out;
position absolute
top 4px
right 10px
img
width 30px
height 30px
position absolute
top 0
bottom 0
margin auto
.chart-toolbox-icons
width 25px
height 25px
margin-left -4px
margin-right -4px
.visual-dl-page-charts:hover
.visual-dl-chart-actions
opacity 1
</style>
import Vue from 'vue'; import Vue from 'vue';
import Router from 'vue-router'; import Router from 'vue-router';
import Scalars from '@/scalars/Scalars'; import Metrics from '@/metrics/Metrics';
import Histogram from '@/histogram/Histogram'; import Samples from '@/samples/Samples';
import Images from '@/images/Images';
import Graph from '@/graph/Graph'; import Graph from '@/graph/Graph';
import Texts from '@/texts/Texts';
import Audio from '@/audio/Audio';
import HighDimensional from '@/high-dimensional/HighDimensional'; import HighDimensional from '@/high-dimensional/HighDimensional';
Vue.use(Router); Vue.use(Router);
...@@ -14,25 +11,17 @@ Vue.use(Router); ...@@ -14,25 +11,17 @@ Vue.use(Router);
export default new Router({ export default new Router({
routes: [ routes: [
{ {
path: '/scalars', path: '/metrics',
name: 'Scalars', name: 'Metrics',
component: Scalars, component: Metrics,
props: (route) => ({ props: (route) => ({
runs: route.query.runs runs: route.query.runs
}) })
}, },
{ {
path: '/histograms', path: '/samples',
name: 'Histograms', name: 'Samples',
component: Histogram, component: Samples,
props: (route) => ({
runs: route.query.runs
})
},
{
path: '/images',
name: 'Images',
component: Images,
props: (route) => ({ props: (route) => ({
runs: route.query.runs runs: route.query.runs
}) })
...@@ -42,22 +31,6 @@ export default new Router({ ...@@ -42,22 +31,6 @@ export default new Router({
name: 'Graph', name: 'Graph',
component: Graph, component: Graph,
}, },
{
path: '/texts',
name: 'Texts',
component: Texts,
props: (route) => ({
runs: route.query.runs
})
},
{
path: '/audio',
name: 'Audio',
component: Audio,
props: (route) => ({
runs: route.query.runs
})
},
{ {
path: '/HighDimensional', path: '/HighDimensional',
name: 'HighDimensional', name: 'HighDimensional',
......
<template>
<div class="visual-dl-page-container">
<div class="visual-dl-page-left">
<div>
<v-card
hover
color="tag_background"
class="visual-dl-tags-tab">
<v-icon>search</v-icon>
<input type="search" v-model="config.groupNameReg"
autocomplete="false"
placeholder="Search tags in RegExp"
class="visual-dl-tags-search-input">
</v-card>
<ui-tags-tab
:total="tagsListCount(allTagsMatchingList)"
:title="config.groupNameReg.trim().length == 0 ? 'All' : config.groupNameReg"
:active="selectedGroup === '' "
@click="selectedGroup = '' "
/>
<ui-tags-tab
v-for="item in groupedTags"
:total="tagsListCount(item.tags)"
:title="item.group"
:active="item.group === selectedGroup"
@click="selectedGroup = item.group"
/>
</div>
<ui-sample-page
:config="config"
:tag-list="finalTagsList"
:total="tagsListCount(finalTagsList)"
/>
</div>
<div class="visual-dl-page-right">
<div class="visual-dl-page-config-container">
<ui-config
:config="config"
/>
</div>
</div>
</div>
</template>
<script>
import {getPluginImagesTags, getPluginAudioTags, getPluginTextsTags} from '../service';
import {cloneDeep, flatten, uniq} from 'lodash';
import autoAdjustHeight from '../common/util/autoAdjustHeight';
import TagsTab from '../common/component/TagsTab';
import Config from './ui/Config';
import SamplePage from './ui/SamplePage';
export default {
name: 'Samples',
components: {
'ui-config': Config,
'ui-sample-page': SamplePage,
'ui-tags-tab': TagsTab,
},
props: {
runs: {
type: Array,
required: true,
},
},
data() {
return {
tagInfo: { image: {}, audio: {}, text: {} },
config: {
groupNameReg: '',
image: { enabled: false, display: false },
audio: { enabled: false, display: false },
text: { enabled: false, display: false },
isActualImageSize: false,
runs: [],
running: true,
},
filteredTagsList: { image: {}, audio: {}, text: {} },
selectedGroup: '',
};
},
computed: {
finalTagsList() {
if (this.selectedGroup === '') {
return this.allTagsMatchingList;
} else {
let list;
this.groupedTags.forEach((item) => {
if (item.group === this.selectedGroup) {
list = item.tags;
}
});
return list;
}
},
allTagsMatchingList() {
let list = cloneDeep(this.filteredTagsList);
this.filteredListByRuns(list);
return list;
},
tagsList() {
let list = {};
Object.keys(this.tagInfo).forEach((type) => {
let tags = this.tagInfo[type];
let runs = Object.keys(tags);
let tagsArray = runs.map((run) => Object.keys(tags[run]));
let allUniqTags = uniq(flatten(tagsArray));
// get the data for every chart
let tagsForEachType = allUniqTags.map((tag) => {
let tagList = runs.map((run) => {
return {
run,
tag: tags[run][tag],
};
}).filter((item) => item.tag !== undefined);
return {
tagList,
tag,
group: tag.split('/')[0],
};
});
list[type] = tagsForEachType;
});
return list;
},
groupedTags() {
let tagsList = this.tagsList || [];
// put data in group
let groupData = {};
Object.keys(tagsList).forEach((type) => {
let tagsForEachType = tagsList[type];
tagsForEachType.forEach((item) => {
let group = item.group;
if (groupData[group] === undefined) {
groupData[group] = {}
}
if (groupData[group][type] === undefined) {
groupData[group][type] = [];
}
groupData[group][type].push(item);
});
});
// to array
let groups = Object.keys(groupData);
let groupList = groups.map((group) => {
this.filteredListByRuns(groupData[group]);
return {
group,
tags: groupData[group],
};
});
return groupList;
},
},
created() {
getPluginImagesTags().then(({errno, data}) => {
if (!data) return;
this.tagInfo.image = data;
this.config.image.enabled = true;
this.config.image.display = true;
this.filterTagsList(this.config.groupNameReg);
});
getPluginAudioTags().then(({errno, data}) => {
if (!data) return;
this.tagInfo.audio = data;
this.config.audio.enabled = true;
this.config.audio.display = true;
this.filterTagsList(this.config.groupNameReg);
});
getPluginTextsTags().then(({errno, data}) => {
if (!data) return;
this.tagInfo.text = data;
this.config.text.enabled = true;
this.config.text.display = true;
this.filterTagsList(this.config.groupNameReg);
});
this.config.runs = this.runs;
},
mounted() {
autoAdjustHeight();
},
watch: {
'config.groupNameReg': function(val) {
this.throttledFilterTagsList();
},
runs: function(val) {
this.config.runs = val;
}
},
methods: {
filterTagsList(groupNameReg) {
if (!groupNameReg || groupNameReg.trim().length == 0) {
this.filteredTagsList = cloneDeep(this.tagsList);
return;
}
this.selectedGroup = '';
let tagsList = this.tagsList || [];
let regExp = new RegExp(groupNameReg);
Object.keys(tagsList).forEach((type) => {
let tagsForEachType = tagsList[type];
this.filteredTagsList[type] = tagsForEachType.filter((item) => regExp.test(item.tag));
});
},
throttledFilterTagsList: _.debounce(
function() {
this.filterTagsList(this.config.groupNameReg);
}, 300
),
filteredListByRuns(list) {
list.image = !this.config.image.display ? [] : this.filteredTypeByRuns(list.image);
list.audio = !this.config.audio.display ? [] : this.filteredTypeByRuns(list.audio);
list.text = !this.config.text.display ? [] : this.filteredTypeByRuns(list.text);
},
filteredTypeByRuns(tagList) {
let runs = this.config.runs || [];
let list = cloneDeep(tagList) || [];
return flatten(list.map((item) => {
return item.tagList.filter((one) => runs.includes(one.run));
}));
},
tagsListCount(tagsList) {
let count = 0;
if (tagsList.image !== undefined) count += tagsList.image.length;
if (tagsList.audio !== undefined) count += tagsList.audio.length;
if (tagsList.text !== undefined) count += tagsList.text.length;
return count;
},
},
};
</script>
<style lang="stylus">
</style>
<template>
<v-card
hover
class="visual-dl-audio">
<h3 class="visual-dl-audio-title">{{ tagInfo.tag.displayName }}
<span class="visual-dl-audio-run-icon">{{ tagInfo.run }}</span>
</h3>
<p>
<span>Step:</span>
<span>{{ audioData.step }}</span>
<span class="visual-del-audio-time">{{ audioData.wallTime | formatTime }}</span>
</p>
<v-slider
:max="steps"
:min="slider.min"
:step="1"
v-model="currentIndex"
/>
<audio
controls
:src="audioData.audioSrc">
Your browser does not support the audio element.
</audio>
</v-card>
</template>
<script>
import {getPluginAudioAudio} from '../../service';
// the time to refresh chart data
const intervalTime = 30;
export default {
props: {
tagInfo: {
type: Object,
required: true,
},
runs: {
type: Array,
required: true,
},
running: {
type: Boolean,
required: true,
},
},
computed: {
steps() {
let data = this.data || [];
return data.length - 1;
},
},
filters: {
formatTime: function(value) {
if (!value) {
return;
}
// The value was made in seconds, must convert it to milliseconds
let time = new Date(value * 1000);
let options = {
weekday: 'short', year: 'numeric', month: 'short',
day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit',
};
return time.toLocaleDateString('en-US', options);
},
},
data() {
return {
currentIndex: 0,
slider: {
value: '0',
label: '',
min: 0,
step: 1,
},
audioData: {},
data: [],
isDemo: process.env.NODE_ENV === 'demo',
};
},
created() {
this.getOriginAudioData();
},
mounted() {
if (this.running && !this.isDemo) {
this.startInterval();
}
},
beforeDestroy() {
this.stopInterval();
},
watch: {
running: function(val) {
(val && !this.isDemo) ? this.startInterval() : this.stopInterval();
},
currentIndex: function(index) {
if (this.data && this.data[index]) {
let currentAudioInfo = this.data ? this.data[index] : {};
let {query, step, wallTime} = currentAudioInfo;
let url = '/data/plugin/audio/individualAudio?ts=' + wallTime;
let audioSrc = [url, query].join('&');
this.audioData = {
audioSrc,
step,
wallTime,
};
}
},
tagInfo: function(val) {
this.currentIndex = 0;
this.getOriginAudioData();
}
},
methods: {
stopInterval() {
clearInterval(this.getOringDataInterval);
},
// get origin data per {{intervalTime}} seconds
startInterval() {
this.getOringDataInterval = setInterval(() => {
this.getOriginAudioData();
}, intervalTime * 1000);
},
getOriginAudioData() {
// let {run, tag} = this.tagInfo;
let run = this.tagInfo.run;
let tag = this.tagInfo.tag;
let {displayName, samples} = tag;
let params = {
run,
tag: displayName,
samples,
};
getPluginAudioAudio(params).then(({status, data}) => {
if (status === 0) {
this.data = data;
this.currentIndex = data.length - 1;
}
});
},
},
};
</script>
<style lang="stylus">
.visual-dl-audio
font-size 12px
width 420px
float left
margin 20px 30px 10px 0
background #fff
padding 10px
.visual-dl-audio-title
font-size 14px
line-height 30px
.visual-dl-audio-run-icon
background #e4e4e4
float right
margin-right 10px
padding 0 10px
border solid 1px #e4e4e4
border-radius 6px
line-height 20px
margin-top 4px
.visual-del-audio-time
float right
</style>
<template>
<div class="visual-dl-page-config-com">
<v-checkbox
class="visual-dl-page-config-checkbox"
label="Image"
v-model="config.image.display"
:disabled="!config.image.enabled"
dark/>
<div class="visual-dl-page-component-block">
<v-checkbox
class="visual-dl-page-subconfig-checkbox"
label="Show actual image size"
v-model="config.isActualImageSize"
dark
:disabled="!config.image.display"/>
</div>
<v-checkbox
class="visual-dl-page-config-checkbox"
label="Audio"
v-model="config.audio.display"
:disabled="!config.audio.enabled"
dark/>
<v-checkbox
class="visual-dl-page-config-checkbox"
label="Text"
v-model="config.text.display"
:disabled="!config.text.enabled"
dark/>
<v-btn
:color="config.running ? 'primary' : 'error'"
v-model="config.running"
v-if="!isDemo"
@click="toggleAllRuns"
class="visual-dl-page-run-toggle"
dark
block
>
{{ config.running ? 'Running' : 'Stopped' }}
</v-btn>
</div>
</template>
<script>
export default {
props: {
config: {
type: Object,
required: true,
},
},
data() {
return {
isDemo: process.env.NODE_ENV === 'demo',
};
},
methods: {
toggleAllRuns() {
this.config.running = !this.config.running;
},
},
};
</script>
<style lang="stylus">
+prefix-classes('visual-dl-page-')
.config-com
padding 20px
.component-block
padding-left 33px
padding-bottom 20px
margin-top -10px
.disabled-text
opacity 0.5
.run-toggle
margin-top 20px
.config-checkbox label
font-size 13px
font-weight bold
.subconfig-checkbox
margin-top 10px
.subconfig-checkbox label
font-size 12px
.input-group--select .input-group__selections__comma
font-size 12px
</style>
<template>
<v-card
hover
class="visual-dl-image">
<h3 class="visual-dl-image-title">{{ tagInfo.tag.displayName }}
<span class="visual-dl-image-run-icon">{{ tagInfo.run }}</span>
</h3>
<p>
<span>Step:</span>
<span>{{ imgData.step }}</span>
<span class="visual-del-image-time">{{ imgData.wallTime | formatTime }}</span>
</p>
<v-slider
:max="steps"
:min="slider.min"
:step="1"
v-model="currentIndex"
/>
<img
:width="imageWidth"
:height="imageHeight"
:src="imgData.imgSrc" >
</v-card>
</template>
<script>
import {getPluginImagesImages} from '../../service';
const defaultImgWidth = 400;
const defaultImgHeight = 300;
// the time to refresh chart data
const intervalTime = 30;
export default {
props: {
tagInfo: {
type: Object,
required: true,
},
runs: {
type: Array,
required: true,
},
running: {
type: Boolean,
required: true,
},
isActualImageSize: {
type: Boolean,
required: true,
},
},
computed: {
steps() {
let data = this.data || [];
return data.length - 1;
},
imageWidth() {
return this.isActualImageSize ? this.imgData.width : defaultImgWidth;
},
imageHeight() {
return this.isActualImageSize ? this.imgData.height : defaultImgHeight;
},
},
filters: {
formatTime: function(value) {
if (!value) {
return;
}
// The value was made in seconds, must convert it to milliseconds
let time = new Date(value * 1000);
let options = {
weekday: 'short', year: 'numeric', month: 'short',
day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit',
};
return time.toLocaleDateString('en-US', options);
},
},
data() {
return {
currentIndex: 0,
slider: {
value: '0',
label: '',
min: 0,
step: 1,
},
imgData: {},
data: [],
height: defaultImgHeight,
weight: defaultImgWidth,
isDemo: process.env.NODE_ENV === 'demo',
};
},
created() {
this.getOriginChartsData();
},
mounted() {
if (this.running && !this.isDemo) {
this.startInterval();
}
},
beforeDestroy() {
this.stopInterval();
},
watch: {
running: function(val) {
(val && !this.isDemo) ? this.startInterval() : this.stopInterval();
},
currentIndex: function(index) {
/* eslint-disable fecs-camelcase */
if (this.data && this.data[index]) {
let currentImgInfo = this.data ? this.data[index] : {};
let {height, width, query, step, wallTime} = currentImgInfo;
let url = '/data/plugin/images/individualImage?ts=' + wallTime;
let imgSrc = [url, query].join('&');
this.imgData = {
imgSrc,
height,
width,
step,
wallTime,
};
}
/* eslint-enable fecs-camelcase */
},
tagInfo: function(val) {
this.currentIndex = 0;
this.getOriginChartsData();
}
},
methods: {
stopInterval() {
clearInterval(this.getOringDataInterval);
},
// get origin data per {{intervalTime}} seconds
startInterval() {
this.getOringDataInterval = setInterval(() => {
this.getOriginChartsData();
}, intervalTime * 1000);
},
getOriginChartsData() {
// let {run, tag} = this.tagInfo;
let run = this.tagInfo.run;
let tag = this.tagInfo.tag;
let {displayName, samples} = tag;
let params = {
run,
tag: displayName,
samples,
};
getPluginImagesImages(params).then(({status, data}) => {
if (status === 0) {
this.data = data;
this.currentIndex = data.length - 1;
}
});
},
},
};
</script>
<style lang="stylus">
.visual-dl-image
font-size 12px
width 420px
float left
margin 20px 30px 10px 0
background #fff
padding 10px
.visual-dl-image-title
font-size 14px
line-height 30px
.visual-dl-image-run-icon
background #e4e4e4
float right
margin-right 10px
padding 0 10px
border solid 1px #e4e4e4
border-radius 6px
line-height 20px
margin-top 4px
.visual-del-image-time
float right
</style>
<template>
<div class="visual-dl-chart-page">
<div class="visual-dl-sample-chart-box">
<ui-image
v-for="(tagInfo, index) in filteredImageTagList"
:key="index"
:tag-info="tagInfo"
:is-actual-image-size="config.isActualImageSize"
:runs="config.runs"
:running="config.running"
/>
</div>
<div class="visual-dl-sample-chart-box">
<ui-audio
v-for="(tagInfo, index) in filteredAudioTagList"
:key="index"
:tag-info="tagInfo"
:runs="config.runs"
:running="config.running"
/>
</div>
<div class="visual-dl-sample-chart-box">
<ui-text
v-for="(tagInfo, index) in filteredTextTagList"
:key="index"
:tag-info="tagInfo"
:runs="config.runs"
:running="config.running"
/>
</div>
<v-pagination
class="visual-dl-sm-pagination"
v-if="total > pageSize"
v-model="currentPage"
:length="pageLength"
/>
</div>
</template>
<script>
import Image from './Image';
import Audio from './Audio';
import Text from './Text';
export default {
components: {
'ui-image': Image,
'ui-audio': Audio,
'ui-text': Text,
},
props: {
config: {
type: Object,
required: true,
},
tagList: {
type: Object,
required: true,
},
total: {
type: Number,
required: true,
},
},
data() {
return {
// current page
currentPage: 1,
// item per page
pageSize: 12,
};
},
computed: {
filteredImageTagList() {
return this.tagList.image.slice((this.currentPage - 1) * this.pageSize, this.currentPage * this.pageSize);
},
filteredAudioTagList() {
let offset = this.tagList.image.length;
let start = (this.currentPage - 1) * this.pageSize - offset;
if (start < 0) start = 0;
let end = this.currentPage * this.pageSize - offset;
if (end < 0) end = 0;
return this.tagList.audio.slice(start, end);
},
filteredTextTagList() {
let offset = this.tagList.image.length + this.tagList.audio.length;
let start = (this.currentPage - 1) * this.pageSize - offset;
if (start < 0) start = 0;
let end = this.currentPage * this.pageSize - offset;
if (end < 0) end = 0;
return this.tagList.text.slice(start, end);
},
pageLength() {
return Math.ceil(this.total / this.pageSize);
},
},
watch: {
'config.runs': function(val) {
this.currentPage = 1;
},
tagList: function(val) {
this.currentPage = 1;
},
},
};
</script>
<style lang="stylus">
@import '~style/variables';
+prefix-classes('visual-dl-')
.chart-page
.sample-chart-box
overflow hidden
float left
.visual-dl-chart-image
float left
.sample-chart-box:after
content ""
clear both
display block
.sm-pagination
height 50px
float left
width 100%
</style>
<template>
<v-card
hover
class="visual-dl-text">
<h3 class="visual-dl-text-title">{{ tagInfo.tag.displayName }}
<span class="visual-dl-text-run-icon">{{ tagInfo.run }}</span>
</h3>
<p>
<span>Step:</span>
<span>{{ textData.step }}</span>
<span class="visual-del-text-time">{{ textData.wallTime | formatTime }}</span>
</p>
<v-slider
:max="steps"
:min="slider.min"
:step="1"
v-model="currentIndex"
/>
<p> {{ textData.message }} </p>
</v-card>
</template>
<script>
import {getPluginTextsTexts} from '../../service';
// the time to refresh chart data
const intervalTime = 30;
export default {
props: {
tagInfo: {
type: Object,
required: true,
},
runs: {
type: Array,
required: true,
},
running: {
type: Boolean,
required: true,
},
},
computed: {
steps() {
let data = this.data || [];
return data.length - 1;
},
},
filters: {
formatTime: function(value) {
if (!value) {
return;
}
// The value was made in seconds, must convert it to milliseconds
let time = new Date(value * 1000);
let options = {
weekday: 'short', year: 'numeric', month: 'short',
day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit',
};
return time.toLocaleDateString('en-US', options);
},
},
data() {
return {
currentIndex: 0,
slider: {
value: '0',
label: '',
min: 0,
step: 1,
},
textData: {},
data: [],
};
},
created() {
this.getOriginChartsData();
},
mounted() {
if (this.running) {
this.startInterval();
}
},
beforeDestroy() {
this.stopInterval();
},
watch: {
running: function(val) {
val ? this.startInterval() : this.stopInterval();
},
currentIndex: function(index) {
if (this.data && this.data[index]) {
let currentTextInfo = this.data ? this.data[index] : {};
let wallTime = currentTextInfo[0];
let step = currentTextInfo[1];
let message = currentTextInfo[2];
this.textData = {
step,
wallTime,
message,
};
}
},
tagInfo: function(val) {
this.currentIndex = 0;
this.getOriginChartsData();
}
},
methods: {
stopInterval() {
clearInterval(this.getOringDataInterval);
},
// get origin data per {{intervalTime}} seconds
startInterval() {
this.getOringDataInterval = setInterval(() => {
this.getOriginChartsData();
}, intervalTime * 1000);
},
getOriginChartsData() {
// let {run, tag} = this.tagInfo;
let run = this.tagInfo.run;
let tag = this.tagInfo.tag;
let {displayName, samples} = tag;
let params = {
run,
tag: displayName,
samples,
};
getPluginTextsTexts(params).then(({status, data}) => {
if (status === 0) {
this.data = data;
this.currentIndex = data.length - 1;
}
});
},
},
};
</script>
<style lang="stylus">
.visual-dl-text
font-size 12px
width 420px
float left
margin 20px 30px 10px 0
background #fff
padding 10px
.visual-dl-text-title
font-size 14px
line-height 30px
.visual-dl-text-run-icon
background #e4e4e4
float right
margin-right 10px
padding 0 10px
border solid 1px #e4e4e4
border-radius 6px
line-height 20px
margin-top 4px
.visual-dl-chart-actions
.sm-form-item
width 300px
display inline-block
.visual-del-text-time
float right
</style>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册