800 lines
30 KiB
PHP
800 lines
30 KiB
PHP
<?php
|
||
/**
|
||
* The model file of chart module of ZenTaoPMS.
|
||
*
|
||
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
|
||
* @license ZPL (http://zpl.pub/page/zplv12.html)
|
||
* @author Chunsheng Wang <chunsheng@cnezsoft.com>
|
||
* @package chart
|
||
* @version $Id: model.php 5086 2013-07-10 02:25:22Z wyd621@gmail.com $
|
||
* @link https://www.zentao.net
|
||
*/
|
||
class chartModel extends model
|
||
{
|
||
/**
|
||
* Construct.
|
||
*
|
||
* @param string $appName
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
public function __construct(string $appName = '')
|
||
{
|
||
parent::__construct($appName);
|
||
$this->loadBIDAO();
|
||
$this->loadModel('bi');
|
||
}
|
||
|
||
/**
|
||
* 判断是否有权限访问。
|
||
* Check chart access.
|
||
*
|
||
* @param int $chartID
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function checkAccess($chartID, $method = 'preview')
|
||
{
|
||
$viewableObjects = $this->bi->getViewableObject('chart');
|
||
if(!in_array($chartID, $viewableObjects))
|
||
{
|
||
return $this->app->control->sendError($this->lang->chart->accessDenied, helper::createLink('chart', $method));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取指定维度下的第一个分组 id。
|
||
* Get the first group id under the specified dimension.
|
||
*
|
||
* @param int $dimensionID
|
||
* @access public
|
||
* @return int|string
|
||
*/
|
||
public function getFirstGroup(int $dimensionID): int|string
|
||
{
|
||
return $this->dao->select('id')->from(TABLE_MODULE)
|
||
->where('deleted')->eq('0')
|
||
->andWhere('type')->eq('chart')
|
||
->andWhere('root')->eq($dimensionID)
|
||
->andWhere('grade')->eq(1)
|
||
->orderBy('`order`')
|
||
->limit(1)
|
||
->fetch('id');
|
||
}
|
||
|
||
/**
|
||
* 获取指定分组下默认显示的图表。
|
||
* Get the charts displayed by default under the specified group.
|
||
*
|
||
* @param int $groupID
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getDefaultCharts(int $groupID): array
|
||
{
|
||
$group = $this->loadModel('tree')->getByID($groupID);
|
||
if(empty($group) || $group->grade != 1) return array();
|
||
|
||
$groups = $this->dao->select('id')->from(TABLE_MODULE)->where('deleted')->eq('0')->andWhere('path')->like(",{$groupID},%")->orderBy('`order`')->fetchPairs();
|
||
if(!$groups) return array();
|
||
|
||
$this->app->loadModuleConfig('screen');
|
||
|
||
$viewableObjects = $this->bi->getViewableObject('chart');
|
||
/* 获取分组下的第一个图表。*/
|
||
/* Get the first chart under the group. */
|
||
foreach($groups as $groupID)
|
||
{
|
||
$chart = $this->dao->select('*')->from(TABLE_CHART)
|
||
->where('deleted')->eq('0')
|
||
->andWhere('builtin', true)->eq('0')
|
||
->orWhere('id')->in($this->config->screen->builtinChart)
|
||
->markRight(1)
|
||
->andWhere("FIND_IN_SET({$groupID}, `group`)")
|
||
->andWhere('stage')->eq('published')
|
||
->andWhere('id')->in($viewableObjects)
|
||
->beginIF(!helper::hasFeature('program'))->andWhere('code')->notLike("%program%")->fi()
|
||
->beginIF(!helper::hasFeature('devops'))->andWhere('code')->notLike("%devops%")->fi()
|
||
->orderBy('id_desc')
|
||
->limit(1)
|
||
->fetch();
|
||
|
||
if($chart)
|
||
{
|
||
$chart = $this->processChart($chart);
|
||
$chart->currentGroup = $groupID;
|
||
|
||
return array($chart);
|
||
}
|
||
}
|
||
|
||
return array();
|
||
}
|
||
|
||
/**
|
||
* 根据 id 获取一个图表。
|
||
* Get a chart by id.
|
||
*
|
||
* @param int $chartID
|
||
* @access public
|
||
* @return object
|
||
*/
|
||
public function getByID(int $chartID): object|false
|
||
{
|
||
$chart = $this->dao->select('*')->from(TABLE_CHART)->where('id')->eq($chartID)->fetch();
|
||
if(!$chart) return false;
|
||
|
||
return $this->processChart($chart);
|
||
}
|
||
|
||
/**
|
||
* 处理图表的数据以供后续使用。
|
||
* Process the data of the chart for subsequent use.
|
||
*
|
||
* @param object $chart
|
||
* @access public
|
||
* @return object
|
||
*/
|
||
public function processChart(object $chart): object
|
||
{
|
||
if($chart->sql == null) $chart->sql = '';
|
||
if($chart->sql) $chart->sql = trim(str_replace(';', '', $chart->sql));
|
||
|
||
$chart->langs = json_decode($chart->langs, true);
|
||
if($chart->langs === null) $chart->langs = array();
|
||
|
||
$chart->filters = json_decode($chart->filters, true);
|
||
if($chart->filters === null) $chart->filters = array();
|
||
|
||
$chart->settings = json_decode($chart->settings, true);
|
||
if($chart->settings === null) $chart->settings = array();
|
||
if(!empty($chart->settings[0]['type']) && empty($chart->type)) $chart->type = $chart->settings[0]['type'];
|
||
|
||
$chart->fieldSettings = json_decode($chart->fields, true);
|
||
if($chart->fieldSettings === null) $chart->fieldSettings = array();
|
||
$chart->fields = array_keys($chart->fieldSettings);
|
||
|
||
return $chart;
|
||
}
|
||
|
||
/**
|
||
* 生成分组和图表混排的菜单树。
|
||
* Generate a menu tree that mixes groups and charts.
|
||
*
|
||
* @param int $groupID
|
||
* @param string $orderBy
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getTreeMenu(int $groupID, string $orderBy = 'id_desc'): array
|
||
{
|
||
if(!$groupID) return array();
|
||
|
||
$group = $this->loadModel('tree')->getByID($groupID);
|
||
if(empty($group) || $group->grade != 1) return array();
|
||
|
||
$groups = $this->dao->select('id, grade, name')->from(TABLE_MODULE)->where('deleted')->eq('0')->andWhere('path')->like("{$group->path}%")->orderBy('`order`')->fetchAll();
|
||
if(!$groups) return array();
|
||
|
||
$this->app->loadModuleConfig('screen');
|
||
|
||
$viewableObjects = $this->bi->getViewableObject('chart');
|
||
/* 获取每个分组下的图表以供生成菜单树。*/
|
||
/* Get the charts under each group for generating the menu tree. */
|
||
$chartGroups = array();
|
||
foreach($groups as $group)
|
||
{
|
||
$chartGroups[$group->id] = $this->dao->select('id, name, builtin, code')->from(TABLE_CHART)
|
||
->where('deleted')->eq('0')
|
||
->andWhere('builtin', true)->eq('0')
|
||
->orWhere('id')->in($this->config->screen->builtinChart)
|
||
->markRight(1)
|
||
->andWhere("FIND_IN_SET({$group->id}, `group`)")
|
||
->andWhere('stage')->eq('published')
|
||
->andWhere('id')->in($viewableObjects)
|
||
->beginIF(!helper::hasFeature('program'))->andWhere('code')->notLike("%program%")->fi()
|
||
->beginIF(!helper::hasFeature('devops'))->andWhere('code')->notLike("%devops%")->fi()
|
||
->orderBy($orderBy)
|
||
->fetchAll();
|
||
}
|
||
if(!$chartGroups) return array();
|
||
|
||
$treeMenu = array();
|
||
foreach($groups as $group)
|
||
{
|
||
if(empty($chartGroups[$group->id])) continue;
|
||
if(!helper::hasFeature('program') && strpos($group->name, $this->lang->program->common) !== false) continue;
|
||
if(!helper::hasFeature('devops') && strpos($group->name, $this->lang->devops->common) !== false) continue;
|
||
|
||
/* 菜单树中只显示二级分组名称。*/
|
||
/* Only the name of the second-level group is displayed in the menu tree. */
|
||
if($group->grade == 2) $treeMenu[] = (object)array('id' => $group->id, 'parent' => 0, 'name' => $group->name);
|
||
|
||
foreach($chartGroups[$group->id] as $chart)
|
||
{
|
||
$treeMenu[] = (object)array('id' => $group->id . '_' . $chart->id, 'parent' => $group->id, 'name' => $chart->name);
|
||
}
|
||
}
|
||
|
||
return $treeMenu;
|
||
}
|
||
|
||
/**
|
||
* 获取图表的 echarts 配置。
|
||
* Get the echarts configuration of the chart.
|
||
*
|
||
* @param object $chart
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getEchartOptions(object $chart): array
|
||
{
|
||
$settings = current($chart->settings);
|
||
$type = $settings['type'];
|
||
$driver = $chart->driver;
|
||
$options = array();
|
||
|
||
$filterFormat = $this->getFilterFormat($chart->filters);
|
||
|
||
/* 过滤掉模板数据。 */
|
||
if(strpos($chart->sql, 'zt_project') !== false) $chart->sql = preg_replace('/\bzt_project\b(?!\w)/', 'ztv_projectnotpl', $chart->sql);
|
||
if(strpos($chart->sql, 'zt_task') !== false) $chart->sql = preg_replace('/\bzt_task\b(?!\w)/', 'ztv_tasknotpl', $chart->sql);
|
||
|
||
if($type == 'pie') $options = $this->genPie($chart->fieldSettings, $settings, $chart->sql, $filterFormat, $driver);
|
||
if($type == 'radar') $options = $this->genRadar($chart->fieldSettings, $settings, $chart->sql, $filterFormat, $chart->langs, $driver);
|
||
if($type == 'line') $options = $this->genLineChart($chart->fieldSettings, $settings, $chart->sql, $filterFormat, $chart->langs, $driver);
|
||
if($type == 'cluBarX' || $type == 'cluBarY') $options = $this->genCluBar($chart->fieldSettings, $settings, $chart->sql, $filterFormat, '', $chart->langs, $driver);
|
||
if($type == 'stackedBar' || $type == 'stackedBarY') $options = $this->genCluBar($chart->fieldSettings, $settings, $chart->sql, $filterFormat, 'total', $chart->langs, $driver);
|
||
if($type == 'waterpolo') $options = $this->bi->genWaterpolo($chart->fieldSettings, $settings, $chart->sql, $filterFormat, $driver);
|
||
|
||
if(empty($options)) return array();
|
||
|
||
$options = $this->addFormatter4Echart($options, $type);
|
||
$options = $this->addRotate4Echart($options, $settings, $type);
|
||
|
||
return $options;
|
||
}
|
||
|
||
/**
|
||
* 为 echart options 添加 formatter。
|
||
*
|
||
* @param array $options
|
||
* @param string $type
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function addFormatter4Echart(array $options, string $type): array
|
||
{
|
||
if($type == 'waterpolo')
|
||
{
|
||
$formatter = "RAWJS<(params) => (params.value * 100).toFixed(2) + '%'>RAWJS";
|
||
$options['series'][0]['label']['formatter'] = $formatter;
|
||
$options['tooltip']['formatter'] = $formatter;
|
||
}
|
||
elseif(in_array($type, $this->config->chart->canLabelRotate))
|
||
{
|
||
$labelMaxLength = $this->config->chart->labelMaxLength;
|
||
$labelFormatter = "RAWJS<(value) => {value = value.toString(); return value.length <= $labelMaxLength ? value : value.substring(0, $labelMaxLength) + '...'}>RAWJS";
|
||
|
||
if(!isset($options['xAxis']['axisLabel'])) $options['xAxis']['axisLabel'] = array();
|
||
if(!isset($options['yAxis']['axisLabel'])) $options['yAxis']['axisLabel'] = array();
|
||
$options['xAxis']['axisLabel']['formatter'] = $labelFormatter;
|
||
$options['yAxis']['axisLabel']['formatter'] = $labelFormatter;
|
||
}
|
||
|
||
return $options;
|
||
}
|
||
|
||
/**
|
||
* 为 echart options 添加 rotate。
|
||
*
|
||
* @param array $options
|
||
* @param string $type
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function addRotate4Echart(array $options, array $settings, string $type): array
|
||
{
|
||
if(in_array($type, $this->config->chart->canLabelRotate))
|
||
{
|
||
if(isset($settings['rotateX']) and $settings['rotateX'] == 'use') $options['xAxis']['axisLabel']['rotate'] = 30;
|
||
if(isset($settings['rotateY']) and $settings['rotateY'] == 'use') $options['yAxis']['axisLabel']['rotate'] = 30;
|
||
}
|
||
|
||
return $options;
|
||
}
|
||
|
||
/**
|
||
* 雷达图。
|
||
* Gen radar.
|
||
*
|
||
* @param array $fields
|
||
* @param array $settings
|
||
* @param string $defaultSql
|
||
* @param array $filters
|
||
* @param array $langs
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function genRadar(array $fields, array $settings, string $defaultSql, array $filters, array $langs = array(), $driver = 'mysql'): array
|
||
{
|
||
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->getMultiData($settings, $defaultSql, $filters, $driver);
|
||
|
||
$yDatas = array();
|
||
$max = 0;
|
||
foreach($yStats as $yStat)
|
||
{
|
||
if(empty($yStat)) continue;
|
||
|
||
$data = array();
|
||
foreach($xLabels as $xLabel)
|
||
{
|
||
$yStatXLabel = isset($yStat[$xLabel]) ? $yStat[$xLabel] : 0;
|
||
$data[] = $yStatXLabel;
|
||
}
|
||
|
||
if(max($yStat) > $max) $max = max($yStat);
|
||
$yDatas[] = $data;
|
||
}
|
||
|
||
$series = array();
|
||
$series['type'] = 'radar';
|
||
foreach($yDatas as $index => $yData)
|
||
{
|
||
$fieldName = $this->chartTao->switchFieldName($fields, $langs, $metrics, $index);
|
||
$seriesName = $fieldName . '(' . $this->lang->chart->aggList[$aggs[$index]] . ')';
|
||
$series['data'][] = array('name' => $seriesName, 'value' => $yData);
|
||
}
|
||
|
||
$indicator = array();
|
||
$optionList = $this->getSysOptions($fields[$group]['type'], $fields[$group]['object'], $fields[$group]['field']);
|
||
foreach($xLabels as $xLabel)
|
||
{
|
||
$labelName = isset($optionList[$xLabel]) ? $optionList[$xLabel] : $xLabel;
|
||
$indicator[] = array('name' => $labelName, 'max' => $max);
|
||
}
|
||
|
||
return array('series' => $series, 'radar' => array('indicator' => $indicator, 'center' => array('50%', '55%')), 'tooltip' => array('trigger' => 'item'));
|
||
}
|
||
|
||
/**
|
||
* 饼图。
|
||
* Gen pie.
|
||
*
|
||
* @param array $fields
|
||
* @param array $settings
|
||
* @param string $sql
|
||
* @param array $filters
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function genPie(array $fields, array $settings, string $sql, array $filters, string $driver = 'mysql'): array
|
||
{
|
||
$group = isset($settings['group'][0]['field']) ? $settings['group'][0]['field'] : '';
|
||
$date = isset($settings['group'][0]['group']) ? zget($this->config->chart->dateConvert, $settings['group'][0]['group']) : '';
|
||
$metric = isset($settings['metric'][0]['field']) ? $settings['metric'][0]['field'] : '';
|
||
$agg = isset($settings['metric'][0]['valOrAgg']) ? $settings['metric'][0]['valOrAgg'] : '';
|
||
|
||
$rows = $this->chartTao->getRows(str_replace(';', '', $sql), $filters, $date, $group, $metric, $agg, $driver);
|
||
$stat = $this->chartTao->processRows($rows, $date, $group, $metric);
|
||
if(empty($date)) arsort($stat);
|
||
|
||
/* 若查询结果大于50条,将50条之后的结果归于其他。*/
|
||
/* If the query results are greater than 50, the results after 50 will be classified as other. */
|
||
$maxCount = 50;
|
||
if(count($stat) > $maxCount)
|
||
{
|
||
$other = array_sum(array_slice($stat, $maxCount));
|
||
$stat = array_slice($stat, 0, $maxCount);
|
||
$stat[$this->lang->chart->other] = $other;
|
||
}
|
||
|
||
$seriesData = array();
|
||
$optionList = $this->getSysOptions($fields[$group]['type'], $fields[$group]['object'], $fields[$group]['field']);
|
||
foreach($stat as $name => $value)
|
||
{
|
||
if(empty($value)) continue;
|
||
|
||
$labelName = isset($optionList[$name]) ? $optionList[$name] : $name;
|
||
$value = round($value, 2);
|
||
|
||
$seriesData[] = array('name' => $labelName, 'value' => $value);
|
||
}
|
||
|
||
$label = array('show' => true, 'position' => 'outside', 'formatter' => '{b} {d}%');
|
||
$series[] = array('data' => $seriesData, 'center' => array('50%', '55%'), 'type' => 'pie', 'label' => $label);
|
||
|
||
$legend = new stdclass();
|
||
$legend->type = 'scroll';
|
||
$legend->orient = 'horizontal';
|
||
$legend->left = 'center';
|
||
$legend->top = 'top';
|
||
|
||
return array('series' => $series, 'legend' => $legend, 'tooltip' => array('trigger' => 'item', 'formatter' => "{b}<br/> {c} ({d}%)"));
|
||
}
|
||
|
||
|
||
/**
|
||
* 折线图。
|
||
* Gen line.
|
||
*
|
||
* @param array $fields
|
||
* @param array $settings
|
||
* @param string $defaultSql
|
||
* @param array $filters
|
||
* @param array $langs
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function genLineChart(array $fields, array $settings, string $defaultSql, array $filters, array $langs = array(), string $driver = 'mysql'): array
|
||
{
|
||
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->getMultiData($settings, $defaultSql, $filters, $driver);
|
||
|
||
$fieldType = $fields[$settings['xaxis'][0]['field']]['type'];
|
||
if($fieldType == 'date') sort($xLabels);
|
||
|
||
$yDatas = array();
|
||
foreach($xLabels as $xLabel)
|
||
{
|
||
foreach($yStats as $index => $yStat)
|
||
{
|
||
if(!isset($yDatas[$index])) $yDatas[$index] = array();
|
||
$yDatas[$index][] = isset($yStat[$xLabel]) ? $yStat[$xLabel] : 0;
|
||
}
|
||
}
|
||
|
||
$optionList = $this->getSysOptions($fields[$group]['type'], $fields[$group]['object'], $fields[$group]['field']);
|
||
foreach($xLabels as $index => $xLabel) $xLabels[$index] = isset($optionList[$xLabel]) ? $optionList[$xLabel] : $xLabel;
|
||
|
||
$series = array();
|
||
foreach($yDatas as $index => $yData)
|
||
{
|
||
$fieldName = $this->chartTao->switchFieldName($fields, $langs, $metrics, $index);
|
||
$seriesName = $fieldName . '(' . $this->lang->chart->aggList[$aggs[$index]] . ')';
|
||
$series[] = array('name' => $seriesName, 'data' => $yData, 'type' => 'line');
|
||
}
|
||
|
||
$grid = array('left' => '3%', 'right' => '4%', 'bottom' => '3%', 'containLabel' => true);
|
||
$xaxis = array('type' => 'category', 'data' => $xLabels, 'axisTick' => array('alignWithLabel' => true));
|
||
$yaxis = array('type' => 'value');
|
||
|
||
return array('series' => $series, 'grid' => $grid, 'xAxis' => $xaxis, 'yAxis' => $yaxis, 'tooltip' => array('trigger' => 'axis'));
|
||
}
|
||
|
||
/**
|
||
* 簇状条形图、堆积条形图。
|
||
* Gen cluBar.
|
||
*
|
||
* @param array $fields
|
||
* @param array $settings
|
||
* @param string $defaultSql
|
||
* @param array $filters
|
||
* @param string $stack
|
||
* @param array $langs
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function genCluBar(array $fields, array $settings, string $defaultSql, array $filters, string $stack = '', array $langs = array(), $driver = 'mysql'): array
|
||
{
|
||
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->getMultiData($settings, $defaultSql, $filters, $driver);
|
||
|
||
$yDatas = array();
|
||
foreach($yStats as $yStat)
|
||
{
|
||
$data = array();
|
||
foreach($xLabels as $xLabel) $data[] = isset($yStat[$xLabel]) ? $yStat[$xLabel] : 0;
|
||
$yDatas[] = $data;
|
||
}
|
||
|
||
$optionList = $this->getSysOptions($fields[$group]['type'], $fields[$group]['object'], $fields[$group]['field']);
|
||
foreach($xLabels as $index => $xLabel) $xLabels[$index] = isset($optionList[$xLabel]) ? $optionList[$xLabel] : $xLabel;
|
||
|
||
|
||
$position = 'top';
|
||
if($settings['type'] == 'stackedBar' or $settings['type'] == 'stackedBarY') $position = 'inside';
|
||
if($settings['type'] == 'cluBarY') $position = 'right';
|
||
$label = array('show' => true, 'position' => $position, 'formatter' => '{c}');
|
||
|
||
$series = array();
|
||
foreach($yDatas as $index => $yData)
|
||
{
|
||
$fieldName = $this->chartTao->switchFieldName($fields, $langs, $metrics, $index);
|
||
$seriesName = $fieldName . '(' . $this->lang->chart->aggList[$aggs[$index]] . ')';
|
||
$series[] = array('name' => $seriesName, 'data' => $yData, 'type' => 'bar', 'stack' => $stack, 'label' => $label);
|
||
}
|
||
|
||
$grid = array('left' => '3%', 'right' => '4%', 'bottom' => '3%', 'containLabel' => true);
|
||
$xaxis = array('type' => 'category', 'data' => $xLabels, 'axisLabel' => array('interval' => 0), 'axisTick' => array('alignWithLabel' => true));
|
||
$yaxis = array('type' => 'value');
|
||
|
||
/* 簇状柱形图和簇状条形图其实只是x轴和y轴换了换,所以交换一下簇状条形图 xAxis和yAxis即可,这样方法就可以复用了。*/
|
||
$isY = in_array($settings['type'], array('cluBarY', 'stackedBarY'));
|
||
if($isY) list($xaxis, $yaxis) = array($yaxis, $xaxis);
|
||
$options = array('series' => $series, 'grid' => $grid, 'xAxis' => $xaxis, 'yAxis' => $yaxis, 'tooltip' => array('trigger' => 'axis'));
|
||
|
||
if(is_array($xLabels) and count($xLabels) > 10)
|
||
{
|
||
$sliderConfig = $this->config->chart->dataZoom->slider;
|
||
$axisIndex = $isY ? 'yAxisIndex' : 'xAxisIndex';
|
||
|
||
$dataZoomCommon = $this->config->chart->dataZoom->common;
|
||
$dataZoomCommon->inside->$axisIndex = array(0);
|
||
$dataZoomCommon->slider->$axisIndex = array(0);
|
||
$dataZoomCommon->slider->width = $sliderConfig->{$isY ? 'width' : 'height'};
|
||
$dataZoomCommon->slider->height = $sliderConfig->{$isY ? 'height' : 'width'};
|
||
$dataZoomCommon->slider->{$isY ? 'top' : 'bottom'} = $sliderConfig->{$isY ? 'top' : 'bottom'};
|
||
$dataZoomCommon->slider->{$isY ? 'right' : 'left'} = $sliderConfig->{$isY ? 'right' : 'left'};
|
||
|
||
$dataZoom = array($dataZoomCommon->inside, $dataZoomCommon->slider);
|
||
|
||
$options['dataZoom'] = $dataZoom;
|
||
}
|
||
|
||
return $options;
|
||
}
|
||
|
||
/**
|
||
* 获取图表所需的数据:X轴、Y轴、计数方式
|
||
* Get multi data.
|
||
*
|
||
* @param array $settings
|
||
* @param string $defaultSql
|
||
* @param array $filters
|
||
* @param bool $sort
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getMultiData(array $settings, string $defaultSql, array $filters, string $driver, bool $sort = false): array
|
||
{
|
||
$group = isset($settings['xaxis'][0]['field']) ? $settings['xaxis'][0]['field'] : '';
|
||
$date = isset($settings['xaxis'][0]['group']) ? zget($this->config->chart->dateConvert, $settings['xaxis'][0]['group']) : '';
|
||
|
||
$metrics = array();
|
||
$aggs = array();
|
||
foreach($settings['yaxis'] as $yaxis)
|
||
{
|
||
$metrics[] = $yaxis['field'];
|
||
$aggs[] = $yaxis['valOrAgg'];
|
||
}
|
||
$yCount = count($metrics);
|
||
|
||
$xLabels = array();
|
||
$yStats = array();
|
||
for($i = 0; $i < $yCount; $i ++)
|
||
{
|
||
$metric = $metrics[$i];
|
||
$agg = $aggs[$i];
|
||
|
||
$rows = $this->chartTao->getRows($defaultSql, $filters, $date, $group, $metric, $agg, $driver);
|
||
$stat = $this->chartTao->processRows($rows, $date, $group, $metric);
|
||
|
||
if($sort) arsort($stat);
|
||
$yStats[] = $stat;
|
||
|
||
$xLabels = array_merge($xLabels, array_keys($stat));
|
||
$xLabels = array_unique($xLabels);
|
||
}
|
||
|
||
return array($group, $metrics, $aggs, $xLabels, $yStats);
|
||
}
|
||
|
||
/**
|
||
* 使用设置的内容,在sql结果中计算百分比
|
||
* Get water polo option.
|
||
*
|
||
* @param array $settings
|
||
* @param string $sql
|
||
* @param array $filters
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function genWaterpolo(array $settings, string $sql, array $filters, string $driver = 'mysql')
|
||
{
|
||
$operate = "{$settings['calc']}({$settings['goal']})";
|
||
$sql = "select $operate count from ($sql) tt ";
|
||
|
||
$moleculeSQL = $sql;
|
||
$denominatorSQL = $sql;
|
||
|
||
$moleculeWheres = array();
|
||
$denominatorWheres = array();
|
||
foreach($settings['conditions'] as $condition)
|
||
{
|
||
$condition = (array)$condition;
|
||
$where = "{$condition['field']} {$this->config->chart->conditionList[$condition['condition']]} '{$condition['value']}'";
|
||
$moleculeWheres[] = $where;
|
||
}
|
||
|
||
if(!empty($filters))
|
||
{
|
||
$wheres = array();
|
||
foreach($filters as $field => $filter)
|
||
{
|
||
$wheres[] = "$field {$filter['operator']} {$filter['value']}";
|
||
}
|
||
$moleculeWheres = array_merge($moleculeWheres, $wheres);
|
||
$denominatorWheres = $wheres;
|
||
}
|
||
|
||
if($moleculeWheres) $moleculeSQL .= 'where ' . implode(' and ', $moleculeWheres);
|
||
if($denominatorWheres) $denominatorSQL .= 'where ' . implode(' and ', $denominatorWheres);
|
||
|
||
$molecule = $this->bi->queryWithDriver($driver, $moleculeSQL, false);
|
||
$denominator = $this->bi->queryWithDriver($driver, $denominatorSQL, false);
|
||
|
||
$percent = $denominator->count ? round($molecule->count / $denominator->count, 4) : 0;
|
||
|
||
$series = array(array('type' => 'liquidFill', 'data' => array($percent), 'color' => array('#2e7fff'), 'outline' => array('show' => false), 'label' => array('fontSize' => 26)));
|
||
$tooltip = array('show' => true);
|
||
$options = array('series' => $series, 'tooltip' => $tooltip);
|
||
|
||
return $options;
|
||
}
|
||
|
||
/**
|
||
* 根据用户设置的字段展示对应的下拉菜单。
|
||
* Get field options.
|
||
*
|
||
* @param string $type user|product|project|execution|dept|option|object|string
|
||
* @param string $object
|
||
* @param string $field
|
||
* @param string $sql
|
||
* @param string $saveAs
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getSysOptions(string $type, string $object = '', string $field = '', string $sql = '', string $saveAs = '', $driver = 'mysql'): array
|
||
{
|
||
if(in_array($type, array('user', 'product', 'project', 'execution', 'dept'))) return $this->bi->getScopeOptions($type);
|
||
if(!$field) return array();
|
||
|
||
$options = array();
|
||
switch($type)
|
||
{
|
||
case 'option':
|
||
$options = $this->bi->getDataviewOptions($object, $field);
|
||
break;
|
||
case 'object':
|
||
$options = $this->bi->getObjectOptions($object, $field);
|
||
break;
|
||
case 'string':
|
||
case 'number':
|
||
if($sql)
|
||
{
|
||
$keyField = $field;
|
||
$valueField = $saveAs ? $saveAs : $field;
|
||
$options = $this->bi->getOptionsFromSql($sql, $driver, $keyField, $valueField);
|
||
}
|
||
break;
|
||
}
|
||
|
||
if($sql and $saveAs and in_array($type, array('user', 'product', 'project', 'execution', 'dept', 'option', 'object')))
|
||
{
|
||
$options = $this->bi->getOptionsFromSql($sql, $driver, $field, $saveAs);
|
||
}
|
||
|
||
return array_filter($options);
|
||
}
|
||
|
||
/**
|
||
* 判断操作按钮是否可点击。
|
||
* Adjust the action is clickable.
|
||
*
|
||
* @param object $chart
|
||
* @param string $action
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public static function isClickable(object $chart, string $action): bool
|
||
{
|
||
$builtinCharts = array();
|
||
$builtinCharts[] = array(1001, 1110);
|
||
$builtinCharts[] = array(10000, 10119);
|
||
$builtinCharts[] = array(10201, 10220);
|
||
$builtinCharts[] = array(20002, 20015);
|
||
$builtinCharts[] = array(30000, 30001);
|
||
|
||
$found = false; // 标记ID是否在范围内
|
||
|
||
foreach ($builtinCharts as $range) {
|
||
$minId = $range[0];
|
||
$maxId = $range[1];
|
||
|
||
if ($chart->id >= $minId && $chart->id <= $maxId) {
|
||
$found = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return !$found && !$chart->builtin;
|
||
|
||
}
|
||
|
||
/**
|
||
* 格式化筛选器。
|
||
* Format filter.
|
||
*
|
||
* @param array $filters
|
||
* @access public
|
||
* @return array
|
||
*/
|
||
public function getFilterFormat(array $filters): array
|
||
{
|
||
$filterFormat = array();
|
||
foreach($filters as $filter)
|
||
{
|
||
$field = $filter['field'];
|
||
$type = $filter['type'];
|
||
if($type != 'condition' && !isset($filter['default'])) continue;
|
||
|
||
if($type != 'condition') $default = $filter['default'];
|
||
switch($type)
|
||
{
|
||
case 'select':
|
||
if(empty($default)) break;
|
||
if(!is_array($default)) $default = array($default);
|
||
$default = array_filter($default, function($val){return !empty($val);});
|
||
$value = "('" . implode("', '", $default) . "')";
|
||
$filterFormat[$field] = array('operator' => 'IN', 'value' => $value);
|
||
break;
|
||
case 'input':
|
||
$filterFormat[$field] = array('operator' => 'like', 'value' => "'%$default%'");
|
||
break;
|
||
case 'date':
|
||
case 'datetime':
|
||
$begin = $default['begin'];
|
||
$end = $default['end'];
|
||
|
||
if(empty($begin) or empty($end)) break;
|
||
|
||
$begin = date('Y-m-d 00:00:00', strtotime($begin));
|
||
$end = date('Y-m-d 23:59:59', strtotime($end));
|
||
|
||
$value = "'$begin' and '$end'";
|
||
$filterFormat[$field] = array('operator' => 'BETWEEN', 'value' => $value);
|
||
break;
|
||
case 'condition':
|
||
$operator = $filter['operator'];
|
||
$value = $filter['value'];
|
||
|
||
if(in_array($operator, array('IN', 'NOT IN')))
|
||
{
|
||
$valueArr = explode(',', $value);
|
||
foreach($valueArr as $key => $val) $valueArr[$key] = '"' . $val . '"';
|
||
$value = '(' . implode(',', $valueArr) . ')';
|
||
}
|
||
elseif(in_array($operator, array('IS NOT NULL', 'IS NULL')))
|
||
{
|
||
$value = '';
|
||
}
|
||
$filterFormat[$field] = array('operator' => $operator, 'value' => $value);
|
||
break;
|
||
}
|
||
}
|
||
|
||
return $filterFormat;
|
||
}
|
||
|
||
/**
|
||
* 在sql中将变量解析为空字符串。
|
||
*
|
||
* @param array $options
|
||
* @param string $type
|
||
* @access public
|
||
* @return bool
|
||
*/
|
||
public function isChartHaveData(array $options, string $type): bool
|
||
{
|
||
if($type == 'waterpolo') return true;
|
||
|
||
$data = array();
|
||
if($type == 'pie') $data = $options['series'][0]['data'];
|
||
if($type == 'line') $data = $options['xAxis']['data'];
|
||
if($type == 'radar') $data = $options['radar']['indicator'];
|
||
if($type == 'cluBarY' or $type == 'stackedBarY') $data = $options['yAxis']['data'];
|
||
if($type == 'cluBarX' or $type == 'stackedBar') $data = $options['xAxis']['data'];
|
||
|
||
return count($data) ? true : false;
|
||
}
|
||
}
|